#!/usr/local/bin/python MYNAME="bookland.py" MYVERSION="0.94" COPYRIGHT="(C) 1999-2005 J. Milgram" DATE = "Jan. 2005" MAINTAINER = "bookland-bugs@cgpp.com" # Copyright (C) 1999-2003 Judah Milgram # # bookland.py - generate Bookland EAN symbol for ISBN and ISMN encoding # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # Because this program copies a portion of itself into its output # file, its output files are also copyright the author and licensed # under the GPL. Relevant provisions of the GPL notwithstanding, # the author licenses users to use and redistribute output files # generated by this program without restriction. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. # # ============================================================================== # # usage: bookland.py [ISBN] [price-code] > output.eps # # ISBN - the ISBN, with or without check digit, with or without hyphens. # default: 1-56592-197-6 ("Programming Python"). If the check digit # is provided on the command line, it is verified. If not, it is # calculated. It's up to you to get the hyphenation right - it's # important, and something the program can't calculate for you. # # price - the five digit add-on code. Usually used to indicate the price, # in which case the first digit indicates the currency (4=$CAN, # 5=$US, etc.). The remaining digits indicate the price, with # decimal point assumed to be between the digit 3 and 4. # For example: $US 6.95 = 50695, $CAN 35.00 = 43500. Instead of a # price code, a 5 digit add-on ranging from 90000-98999 can be # used for internal purposes. BISG recommends just using 90000 if # you don't want to specify a price. Add-ons ranging from 99000 to # 99999 have been reserved for special use. # # An Encapsulated Postscript file (eps) is sent to standard out. This may in turn # be converted to other formats using the pbmplus package. You may have trouble # getting the OCRB to map correctly. If you already have the font, you can look in # the Fontmap file to see what your system calls it, and edit the fontnames accordingly # (see below). If you don't have it, you might find it on your DOS system. You # need a .pfa/.pfb (Type 1) or .ttf (TrueType). Your Postscript interpreter might # or might not be able to deal with TrueType. In any event, in an emergency, you # might get away with Helvetica. Note that as of 1990 BISG no longer requires the # ISBN to be printed in OCR-A. # # Take the "no-warranty" disclaimer seriously. Going to print with a faulty bar # can cost you a bundle, and you'll be on your own. It's up to you to verify that # the symbol is valid. If you need "corporate accountability", try the Book # Industry Study Group at (212) 929-1393 or the US ISBN Agency at (908) 665-6770 # and ask for a list of commercial vendors. Outside the US, don't know. # # Feedback welcome. If you discover a case where the program generates a faulty # symbol, I definitely want to hear about it - write me at milgram@cgpp.com or # P.O. Box 8376, Langley Park, MD 20787, USA # # INSTALLATION: # # If you have a Python interpreter on your system, you're done. Just put this file # somewhere in your path and give it execute permission. If you haven't installed # Python, see http://www.python.org. It has been ported to Macs, DOS, and MS-Windows. # # ABOUT THE BOOKLAND EAN # # The most difficult part of this project was finding the documents that define # the Bookland EAN. There appears to be no single, authoritative source that # provides all the information required. Some good sources: # # [1] "Machine-Readable Coding Guidelines for the U.S. Book Industry", Book # Industry Study Group, New York, Jan., 1992. (212) 929-1393 # [2] "UPC Symbol Specification Manual", Uniform Code Council Inc., # Dayton, Ohio, January 1986 (May 1995 Reprint). (937) 435-3870; I found it # at http://www.uc-council.org/d36-t.htm # [3] "EAN Identification for Retail/Trade Items", EAN International. I found it # in Feb. 1999 at http://www.ean.be/html/Numbering.html # [4] "Hyphenation Instructions", web page at: # http://www.isbn.org/standards/home/isbn/international/hyphenation-instructions.asp # # The starting point of the exercise is the ISBN, assigned by the national ISBN # Agency. This is a 10 digit number, the last being a check digit. The ISBN is # converted to a 13 digit EAN number. The first three digits of the EAN-13 indicate # the country or region. A prefix of 978 has been assigned to books, regardless # of country of origin (hence, "Bookland") [3]. The remaining ten digits are the # first 9 digits of the ISBN followed by the EAN-13 check digit. # # It seems the EAN-13 check digit can be calculated using the same algorithm as the # UPC Version A number. Note that the EAN-13 check digit is always between 0 and 9, # compare with ISBN check digit which can range to 10 ("X"). See Reference [2], # Section 2 and Appendix G for details of creation of the EAN-13 symbol. Table 2 of # Appendix G provides a good comparison of the UPC-A and EAN-13 symbols. # # The 5 digit add-on (here called, "UPC5") is defined in Ref. [2] Appendix D. # The ">" to the right of the five digit code serves to enforce the "quiet zone" to # the right of the bar pattern. Can't remember where I read that. It's probably # optional. According to [1], in the UK, three horizontal bars appear over price # add-ons. Haven't implemented that here. The UPC5 encoding is based on UPC-A and # UPC-E. # # According to [2], Section 3, the EAN-13 numbers and 5-digit add-ons are supposed # to be printed in OCR-B. The ISBN itself is printed above the EAN-13 symbol. At # one time it was to be printed in OCR-A, but as of 1990 this requirement has been # dropped [1], and I assume this means you can use any font you like. # # SEE ALSO: # # "TinyHelp 5 - Making ISBN Barcodes", D. Byram-Wigfield. Another approach to making # the ISBN barcode symbol. I saw it at # http://www.cappella.demon.co.uk/index.html/ # but haven't tried it. # # "XBarcode" - nice open-source X-Windows program for generating all sorts of bar codes. # It does much more than this program, but didn't seem to do the UPC # 5-digit add-on or do the ISBN->EAN13 calculation (as of v. 2.11). Might # have made more sense to add this capability, but I needed a Python project. # In any event, their license forbids distribution in modified form! # # HYPHENATION # # bookland.py includes automatic hyphenation for ISBN's in group 0 and 1 # (English-language). This is based on my reading of Ref [4]. If in doubt, # users can use the "-y" option to force the program to accept the hyphenation # as input. For other ISBN groups and for ISMN's, no hyphenation is performed # other than to ensure a hyphen is placed after the group identifier and before # the check digit. # # ABOUT THE ISMN: # # See the ISMN Users' Manual, 3rd Edition, 1998, ISBN 3-88053-070-X, published by # the International ISMN Agency, Staatsbibliothek Preussischer Kulturbesitz, Berlin. # I found my copy at http://www.ismn.spk-berlin.de/download/ISMNmanual.pdf # # An ISMN is just like an ISBN, except: # - first character is an "M" # - the "M" counts as a "3" for computing the ISMN check digit (last digit) # - the checksum weights are 3,1,3,1,3,1,3,1,3, sum to be divisible by "10". This # means the last character is always a numerical digit, never an "X". # - the EAN number is "979" plus the *entire* ten character ISMN, except the # "M" is replaced by "0". Note this means the ISMN checksum is identical to the # corresponding EAN-13 checksum (excercise left to the reader). # # When bookland.py detects an "M" in the first position of the ISBN, it interprets # it as an ISMN and proceeds accordingly. The 5-digit price code symbol is suppressed. # # BAR WIDTH REDUCTIONS # # Starting Version 0.92, the widths of the individual bars can be reduced using the # "-r" option (units are inches). This is to compensate for bleed during printing. # I don't know when it's a good idea to actually use this; in any event consult with # your printer first. If not input, it defaults to zero (no reduction). # # ABOUT PYTHON # # See http://www.python.org # # TO DO: # # - Generalize to more bar codes, starting with UPC-A and UPC-E. "Plain" EAN13 is # already built in, could add command line argument to generate that instead of # Bookland. # - Make font sizes and placement easier to configure - not sure I have it right. # Does human-readable 5-digit code take wider font spacing? # - Clean up bounding box stuff. # - Bells and whistles. # - GUI? # # HISTORY: # # 1/2005 - v 0.94 Add facility to print 13 digit ISBN on top of bars instead of # 10 digit ISBN. Don't use this until after 1 Jan 2007, per # the 13-digit implementation guidelines at # www.isbn-international.org/en/download/implementation-guidelines-04.pdf # # Fixed EAN-13 "right" digits to scale to available space, to # make consistent with the way we handled the "left" digits. # 9/2003 - v 0.93 added provision to license to cover program output # 1/2002 - v 0.92 add ISMN support (thanks to Lars-Henrik Nysten for this suggestion) # re-wrote bar generation to preclude possibility of white hairlines # between adjacent black modules. Thanks to Tero Lindfors for # reporting this bug. # new -o option to write eps to file rather than stdout # new -x option for "check only" (verifies check digit) # new -r option for bar width reduction (compensate for print bleed) # new -s option to scale module (bar) height (Lars-Henrik Nysten again) # can suppress UPC-5 price code by entering empty string. (thanks to # Jacques Du Pasquier for this suggestion) # re-wrote ISBN/ISMN sanity checks # lowercase alphas ("x" for ISBN and "m" for ISMN) now ok on input # fix "long" command line options. # 10/2001 - v 0.91 add -z option for quiet zone ">" # add -f option for fonts # re-write command line parsing to use getopt # 1/2000 - v 0.09 eliminate use of eval # 9/99 - v 0.08 accomodate different versions of OCRB by fitting # all strings to prescribed width. Thanks to Toby Gadd # for reporting this problem and Peter Deutsch for # help finding the fix. # 7/99 -v0.05-0.07 improve error handling. # 3/27/99 - v0.04 add "--help" and "--version". # 3/13/99 - v0.03, do a showpage at end (it's allowed) # fixed checksum calculations for certain cases # 2/7/99 - v0.02, fixed LH parity pattern for EAN13. It's not the check digit! # 2/7/99 - initial release # ================================================================================ # # barCodeSymbol - the whole printed symbol, including bar code(s) and product code(s). # UPC, UPCA, UPC5, EAN13 - the number itself, with check digit, string representation, # and barcode bits # import re # we should get rid of regsub and regex in favor of re # hope there's no conflict. import string import sys import regsub from regex_syntax import * import regex regex.set_syntax(RE_SYNTAX_AWK) from types import * BooklandError = "Something wrong" A="A";B="B";C="C";O="O";E="E" UPCABITS = [{O:"0001101",E:"1110010"}, {O:"0011001",E:"1100110"}, {O:"0010011",E:"1101100"}, {O:"0111101",E:"1000010"}, {O:"0100011",E:"1011100"}, {O:"0110001",E:"1001110"}, {O:"0101111",E:"1010000"}, {O:"0111011",E:"1000100"}, {O:"0110111",E:"1001000"}, {O:"0001011",E:"1110100"}] UPCAPARITY = [ "OOOOOOEEEEEE" ] * 10 UPCEBITS = [{O:"0001101",E:"0100111"}, {O:"0011001",E:"0110011"}, {O:"0010011",E:"0011011"}, {O:"0111101",E:"0100001"}, {O:"0100011",E:"0011101"}, {O:"0110001",E:"0111001"}, {O:"0101111",E:"0000101"}, {O:"0111011",E:"0010001"}, {O:"0110111",E:"0001001"}, {O:"0001011",E:"0010111"}] # what about UPCEPARITY? Don't need for isbn. UPC5BITS = UPCEBITS UPC5PARITY = ["EEOOO","EOEOO","EOOEO","EOOOE","OEEOO", "OOEEO","OOOEE","OEOEO","OEOOE","OOEOE"] EAN13BITS = [{A:"0001101", B:"0100111", C:"1110010"}, {A:"0011001", B:"0110011", C:"1100110"}, {A:"0010011", B:"0011011", C:"1101100"}, {A:"0111101", B:"0100001", C:"1000010"}, {A:"0100011", B:"0011101", C:"1011100"}, {A:"0110001", B:"0111001", C:"1001110"}, {A:"0101111", B:"0000101", C:"1010000"}, {A:"0111011", B:"0010001", C:"1000100"}, {A:"0110111", B:"0001001", C:"1001000"}, {A:"0001011", B:"0010111", C:"1110100"}] EAN13PARITY = map(lambda x: x+"CCCCCC", ["AAAAAA","AABABB","AABBAB","AABBBA","ABAABB", "ABBAAB","ABBBAA","ABABAB","ABABBA","ABBABA"]) PSFORMAT = "%.6f" # Default fonts. # Fonts might have a different name on your system. # Edit if required. ISBNFONT = "OCRB" # Doesn't have to be OCR-B EAN13FONT = "OCRB" UPC5FONT = "OCRB" class psfile: def __init__(self): self.x0 = 100; self.y0 = 100 self.lines=[] self.bb=[self.x0,self.y0,self.x0,self.y0] def orbb(self,arg): self.bb[0] = min(self.bb[0],self.x0+arg[0]) self.bb[1] = min(self.bb[1],self.y0+arg[1]) self.bb[2] = max(self.bb[2],self.x0+arg[2]) self.bb[3] = max(self.bb[3],self.y0+arg[3]) def translate(self,dx,dy): self.x0 = self.x0 + dx self.y0 = self.y0 + dy return "%d %d translate 0 0 moveto" % (dx,dy) def out(self,file=None): if file: outfid=open(file,"w") else: outfid=sys.stdout for line in self.lines: outfid.write("%s\n"%line) outfid.close() def do(self,arg): self.lines = self.lines + arg def setbb(self): for i in range(len(self.lines)): if self.lines[i]=="%%BoundingBox: TBD": self.lines[i]= "%%BoundingBox:" + \ " %d"%self.bb[0] + \ " %d"%self.bb[1] + \ " %d"%self.bb[2] + \ " %d"%self.bb[3] return def header(self,title,comments,ean13font,isbnfont,upc5font): for i in range(len(comments)): comments[i] = regsub.gsub("^","% ",comments[i]) # There's a more elegant way to do the bounding box line: return [ "%!PS-Adobe-2.0 EPSF-1.2", "%%Creator: " + MYNAME + " " + MYVERSION + " " + DATE, "%%Title: " + title, "%%BoundingBox: TBD", "%%EndComments" ] +\ comments + \ [ "\n% These font names might be different on your system:", "/ean13font { /" + ean13font + " findfont 10 scalefont setfont } def", "/isbnfont { /" + isbnfont + " findfont 8 scalefont setfont } def", "/upc5font { /" + upc5font +" findfont 14 scalefont setfont } def\n", "/nextModule { moduleWidth 0 rmoveto } def", "% The following shenanigans is to deal with different implementations", "% of same font having different char sizes and spacing.", "% function fitstring:", "% usage: width string font fitstring", "% set font, scaled so that string exactly fits desired width", "% leave string on stack", "/fitstring { dup findfont 1 scalefont setfont % w s f", "3 1 roll % f w s", "dup stringwidth pop % f w s sw", "3 2 roll exch div % f s x", "3 2 roll findfont exch scalefont setfont", "} def", "/barHeight { 72 } def", "/nextModule { moduleWidth 0 rmoveto } def", "/topcentershow {dup stringwidth pop neg 2 div -9 rmoveto show} def", "/toprightshow {dup stringwidth pop neg -9 rmoveto show} def", "/bottomcentershow {dup stringwidth pop neg 2 div 0 rmoveto show} def", "/bottomrightshow {dup stringwidth pop neg 0 rmoveto show} def", "/W { moduleWidth mul 0 rmoveto } def", "/B { dup moduleWidth mul 2 div 0 rmoveto", "dup moduleWidth mul barWidthReduction sub setlinewidth", "0 barHeight rlineto 0 barHeight neg rmoveto", "currentpoint stroke moveto", "moduleWidth mul 2 div 0 rmoveto } def", "/L { dup moduleWidth mul 2 div 0 rmoveto", "dup moduleWidth mul barWidthReduction sub setlinewidth", "0 -5 rmoveto 0 5 rlineto", "0 barHeight rlineto 0 barHeight neg rmoveto", "currentpoint stroke moveto", "moduleWidth mul 2 div 0 rmoveto } def", self.x0,self.y0,"translate", "0 0 moveto" ] def trailer(self): return ["stroke","% showpage supposedly OK in EPS", "showpage","\n% Good luck!"] class UPC: # Includes UPC-A, UPC-E, EAN-13 (sorry), UPC-5 et al. def __init__(self,arg): # arg is a string, either: # - product code including checksum # - same, with hyphens (hyphens not verified) # - same, but with last digit (checksum) dropped, possibly leaving a # trailing hyphen. # If checksum is included, it will be verified. # N.B. "integer" representation is still a string! Just has no hyphens. self.s=arg self.verifyChars(self.s) self.n = regsub.gsub("-","",self.s) # create "integer" representation self.x = self.checkDigit(self.n) # always calculate check digit if len(self.n) == self.ndigits: self.verifyCheckDigit() # if check digit given, verify it elif len(self.n) == self.ndigits-1: self.tackonCheckDigit() # tack on check digit else: raise BooklandError, "UPC: wrong number of digits in \"" + self.s + "\"" def setbits(self,arg): # UPC (all) self.bits="" parityPattern=self.parityPattern() bitchar=self.bitchar() for p in range(len(arg)): digit=int(arg[p]) # maybe better to define parityPattern with a leading blank? parity=parityPattern[p] bit=bitchar[digit][parity] self.bits=self.bits + bit def verifyChars(self,s): # UPC (all) # Trailing hyphen allowed. nevergood = "--|^-|[^0-9-]" ierr=regex.search(nevergood,s) if ierr != -1: raise BooklandError, \ "UPCA: in %s: illegal characters beginning with: %s" % (s,s[ierr]) def verifyCheckDigit(self): # UPC (all) # first verify correct number of digits. soll=self.checkDigit(self.n) ist=self.s[-1:] if ist != soll: raise BooklandError, "For %s checksum %s is wrong, should be %s" % \ (self.s,ist,soll) def xstring(self,p): # UPC (all) return "%d" % p def tackonCheckDigit(self): self.n = self.n + self.x # UPC (all) self.s = self.s + self.x class UPCA(UPC): def __init__(self,arg): UPC.__init__(self,arg) self.setbits(self.n[1:]) # skip first digit def parityPattern(self): return UPCAPARITY[int(self.x)] def bitchar(self): return UPCABITS def checkDigit(self,arg): # UPCA/EAN13 weight=[1,3]*6; magic=10; sum = 0 for i in range(12): # checksum based on first 12 digits. sum = sum + int(arg[i]) * weight[i] z = ( magic - (sum % magic) ) % magic if z < 0 or z >= magic: raise BooklandError, "UPC checkDigit: something wrong." return self.xstring(z) class ISBN: # Includes ISMN, if the plan falls together. # This is an old-style 10 digit ISBN! def __init__(self,arg): self.ndigits=10 # Includes check digit! self.s=string.upper(arg) self.n=re.sub("[ -]","",self.s) # "integer" representation # In ISMN, I allow spaces in place of hyphens. See ISMN User's manual. if re.match("^M( |-)?\d(( |-)?\d){7,7}(-| )?\d?$",self.s): # ISMN self.name="ISMN" self.n=re.sub("^M","3",self.n) self.weight=[3,1,3,1,3,1,3,1,3] self.magic=10 elif re.match("^\d-?\d(-?\d){7,7}-?(\d|X)?$",self.s): # ISBN self.name="ISBN" self.weight=[10,9,8,7,6,5,4,3,2] self.magic=11 else: raise BooklandError, "%s invalid (hyphenation, characters, or length)" % self.s self.x = self.checkDigit() if len(self.n) == self.ndigits: self.verifyCheckDigit() # if check digit given, verify it elif len(self.n) == self.ndigits-1: self.tackonCheckDigit() # tack on check digit else: raise BooklandError, "%s failed. Please report as bug" % self.s def checkDigit(self): # ISBN and ISMN; UPCA/EAN13 similar but for weights etc. # now that we're checking regex's in init, we don't have to check the # argument at all. (used to check length and bad characters) sum = 0 for i in range(9): # checksum based on first nine digits. sum = sum + int(self.n[i]) * self.weight[i] z = ( self.magic - (sum % self.magic) ) % self.magic if z < 0 or z >= self.magic: raise BooklandError, \ "%s: checksum %d is wrong - please report as bug" % (self.s,z) return self.xstring(z) def xstring(self,p): if p == 10: return "X" else: return "%d" % p def tackonCheckDigit(self): if self.s[-1:] == "-": # Already have a trailing hyphen self.s = self.s + self.x else: self.s = self.s + "-" + self.x def verifyCheckDigit(self): # UPC A; EAN13 # first verify correct number of digits. soll=self.x ist=self.s[-1:] if ist != soll: raise BooklandError, \ "For %s checksum %s is wrong, should be %s\n" % (self.s,ist,soll) # We'll use this later when we have to start printing the 13-digit ISBN's over # the bars (after 1 Jan 2007). def isbn13(self): # Return the 13 digit ISBN string based on the 10-digit (978) prefix ISBN. # Except if it's an ISMN, we make no conversion. # I wonder if there is going to be a 13 digit ISMN. if self.name=="ISMN": return self.s else: arg = "978-" + self.s[:-1] return EAN13(arg).s class Bar: # a run of adjacent modules of identical value. def __init__(self,val): self.val=val if not self.val in "L01": raise BooklandError, "bar bit: %s, pls report as a bug" % self.val self.width=1 if self.val=="1": self.color="Black" elif self.val=="0": self.color="White" elif self.val=="L": self.color="Long Black" def __cmp__(self,other): if self.val==other or (self.val=="L" and other=="1"): return 0 else: return 1 def inc(self): self.width=self.width+1 def pslines(self): if self.val=="L": rval = [ "%d L " % self.width ] elif self.val=="1": rval = [ "%d B " % self.width ] else: rval = [ "%d W " % self.width ] return rval def __repr__(self): return "%s bar of width %d" % (self.color,self.width) class barCodeSymbol: def __init__(self): self.patternWidth = len(self.bits)*self.moduleWidth # Anything else? def bitsComment(self): return [ "%% Bits:\n%% %s" % self.bits ] def psbars(self): # new version, try to prevent all hairlines between adjacent modules. bars = [] bar=Bar(self.bits[0]) for bit in self.bits[1:]: if bit==bar: bar.inc() else: bars.append(bar) bar=Bar(bit) bars.append(bar) rval = ["0 0 moveto"] for bar in bars: rval = rval + bar.pslines() rval = rval + [ "stroke" ] return rval def psbarsold(self): psbits=regsub.gsub("1","I ",self.bits) psbits=regsub.gsub("0","O ",psbits) psbits=regsub.gsub("L","L ",psbits) linewidth=50 p=0; j=linewidth; m=len(psbits); psbarlines=[]; blanks="^ | $" while p <= m: j = min(linewidth,m-p) psbarlines = psbarlines + [ regsub.gsub(blanks,"",psbits[p:p+j]) ] p=p+linewidth return [ "0 0 moveto" ] + psbarlines + [ "stroke" ] def psSetBarHeight(self): return [ "/barHeight { " + PSFORMAT % self.moduleHeight + " 72 mul } def" ] def psSetBarWidthReduction(self): return [ "/barWidthReduction { " + \ PSFORMAT % self.barWidthReduction + " 72 mul } def" ] def psSetModuleWidth(self): rval = [ "/moduleWidth { " + PSFORMAT % self.moduleWidth + " 72 mul } def" ] return rval def psBottomRightText(self,text,font): # this is specifically for the upc5 price code. # this is all starting to get messy. return [ PSFORMAT % self.patternWidth + " 72 mul dup 2 div", PSFORMAT % self.moduleHeight + " 72 mul 2 add moveto", "(" + text + ") /" + font + " fitstring bottomcentershow" ] def psTopCenterText(self,text,font,fit=0): # the text at the top center of the bar pattern (i.e. the ISBN) # Set fit=1 if you want the string fitted to the barcode width. rval = [ PSFORMAT % self.patternWidth + " 72 mul" ] if fit: rval.append("dup") rval.append("2 div") rval.append(PSFORMAT % self.moduleHeight + " 72 mul 3 add moveto") rval.append("(%s)" % text) if fit: rval.append("/%s fitstring" % font) rval.append("bottomcentershow") return rval def psFittedText(self,width,text,font): return [ PSFORMAT % width + " (" + text + ") " + font + " fitstring" ] # This is optional; serves to enforce quiet zone to right of UPC 5 add-on def psGreaterThan(self,font): return [ PSFORMAT % self.patternWidth + " 72 mul", PSFORMAT % self.moduleHeight + " 72 mul 2 add moveto", "/%s (>) show" % font ] class EAN13Symbol(barCodeSymbol): def __init__(self,arg,font=EAN13FONT,heightMultiplier=1,barWidthReduction=0): # arg is a string with the EAN product code self.barWidthReduction=barWidthReduction self.ean13 = EAN13(arg) self.moduleWidth = 0.0130 specModuleHeight = 1.00 self.moduleHeight = 1.00 * heightMultiplier self.bits = self.ean13.bits barCodeSymbol.__init__(self) self.font=font def bb(self): return [ -12, -10, self.patternWidth*72+10, self.moduleHeight*72+12 ] def pslines(self): return self.bitsComment() + \ self.psSetModuleWidth() + \ self.psSetBarWidthReduction() + \ self.psSetBarHeight() + \ self.psbars() + \ self.psLRDigitLines() def psLRDigitLines(self): # 24 = 3+6*7/2 # 70 = 3+6*7+4+6*7/2 "4" so we center on the "L" bars (the rightmost of # the center guard bars is an "O". # "5" in check digit is the five-module spacing recommended by [2], section 3. return [ "% We do the left digits first and leave the font scaled", "% as is for the 9-digit and the right-digits.", "% EAN13 Left Digits:", "moduleWidth 24 mul 0 moveto", "moduleWidth 40 mul (" + self.ean13.leftDigits + ") ", "/" + self.font + " fitstring topcentershow", "\n% EAN13 human-readable number", "% The \"9\" digit (only when encoding ISBN's and ISMN's, I think):", "-5 0 moveto (" + self.ean13.n[0] + ") toprightshow", "% EAN13 Right Digits:", "moduleWidth 70 mul 0 moveto", "moduleWidth 40 mul (" + self.ean13.rightDigits + ") ", "/" + self.font + " fitstring topcentershow" ] class EAN13(UPCA): def __init__(self,arg): self.ndigits=13 # Includes check digit! UPCA.__init__(self,arg) leftBits = self.bits[0:42] rightBits = self.bits[42:] leftGuard="L0L" rightGuard="L0L" center="0L0L0" self.bits = leftGuard + leftBits + center + rightBits + rightGuard self.leftDigits = self.n[1:7] self.rightDigits = self.n[7:13] def parityPattern(self): # N.B. parity pattern based on leftmost digit, the UCC Spec calls this # the "13th" digit. It's not the check digit! return EAN13PARITY[int(self.n[0])] def bitchar(self): return EAN13BITS class UPC5Symbol(barCodeSymbol): def __init__(self,arg,heightMultiplier=1,barWidthReduction=0): # arg is a string with the 5 digit add-on. self.barWidthReduction=barWidthReduction self.upc5 = UPC5(arg) self.moduleWidth = 0.0130 specModuleHeight = 0.852 self.moduleHeight = 0.852 * heightMultiplier self.bits = self.upc5.bits barCodeSymbol.__init__(self) def pslines(self): return self.bitsComment() + \ self.psSetModuleWidth() + \ self.psSetBarHeight() + \ self.psbars() def bb(self): # Note quiet zone is there even if we don't print the ">". return [ 0, 0, self.patternWidth*72+10, self.moduleHeight*72+10 ] UPC5Error = "Something wrong with 5-digit price code add-on." class UPC5(UPC): def __init__(self,arg): self.ndigits=5 # Includes check digit! p=re.search("[^0-9]",arg) if p: badchar=arg[p.start()] raise UPC5Error, "\"%s\" is wrong. The character \"%s\" is not allowed. Price code add-on should contain %d digits and nothing else. Or leave blank to suppress the UPC-5 code." % (arg,badchar,self.ndigits) elif len(arg) != self.ndigits: raise UPC5Error, \ "\"%s\" is wrong. Price code add-on should have exactly %d digits." % (arg,self.ndigits) UPC.__init__(self,arg) self.setbits(self.n) leftGuard="1011" # no right guard for UPC 5-digit add-on # Have to insert pesky delineators: delineator = "01" self.bits = leftGuard + \ self.bits[0:7] + delineator + \ self.bits[7:14] + delineator + \ self.bits[14:21] + delineator + \ self.bits[21:28] + delineator + \ self.bits[28:35] def checkDigit(self,arg): # UPC5 weight=[3,9,3,9,3]; sum = 0 for i in range(5): sum = sum + int(arg[i]) * weight[i] return self.xstring(sum % 10) def verifyCheckDigit(self): # UPC2/5 checksum not in number return def parityPattern(self): return UPC5PARITY[int(self.x)] def bitchar(self): return UPC5BITS class bookland(barCodeSymbol): def __init__(self,isbn,price="",*rest): # Some defaults: ean13font=EAN13FONT isbnfont=ISBNFONT upc5font = UPC5FONT zone=None heightMultiplier=1.0 commandLine="" barWidthReduction=0 # Maybe different fonts: if len(rest)>0: font=rest[0] if font: ean13font=font isbnfont=font upc5font=font if len(rest)>1: zone=rest[1] if len(rest)>2: heightMultiplier=float(rest[2]) if len(rest) > 3: commandLine=rest[3] if len(rest) > 4: barWidthReduction=float(rest[4]) # Initial setup: self.ps = psfile() self.isbn = ISBN(isbn) # Header, EAN13 bars, EAN13 number, and ISBN: if self.isbn.name=="ISMN": self.ean13Symbol = EAN13Symbol("9790"+self.isbn.n[1:9],ean13font,heightMultiplier,barWidthReduction) elif self.isbn.name=="ISBN": self.ean13Symbol = EAN13Symbol("978"+self.isbn.n[:9],ean13font,heightMultiplier,barWidthReduction) else: raise BooklandError, "Internal error doing %s, please report as bug" % isbn self.ps.orbb(self.ean13Symbol.bb()) comments = ["", " This is free software and comes with NO WARRANTY WHATSOVER", " Think twice before going to press with this bar code!", "", " This Postscript program contains portions bookland.py, a", " GPL-licensed free program, and is therefore itself licensed", " under the GPL. Relevant provisions of the GPL notwithstanding,", " the author licenses users to use and redistribute output", " files generated by this program without restriction.", "", "Command line: %s" % commandLine, "" ] # Note we use fit=1 # later when we go to printing isbn13() instead of isbn.s, we'll remove the fit # to keep the line from getting too tiny. self.ps.lines = self.ps.header(self.isbn.s,comments,ean13font,isbnfont,upc5font) + \ [ "ean13font" ] + \ self.ean13Symbol.pslines() +\ [ "isbnfont" ] + \ self.ean13Symbol.psTopCenterText("%s %s" % (self.isbn.name,self.isbn.s),isbnfont,fit=1) # 5-digit add-on: (optional for ISBN only) BLANK=re.compile("^ *$") if self.isbn.name=="ISBN" and not BLANK.match(price): # 105 = 95 + 10; 10 = separation (min is 9) translate=[ self.ps.translate( self.ean13Symbol.moduleWidth * 72 * 105, 0 ) ] self.upc5Symbol = UPC5Symbol(price,heightMultiplier,barWidthReduction) self.ps.orbb(self.upc5Symbol.bb()) self.ps.lines = self.ps.lines + \ translate + \ self.upc5Symbol.pslines() + \ [ "upc5font" ] +\ self.upc5Symbol.psBottomRightText(price,upc5font) if zone: self.ps.lines=self.ps.lines + self.upc5Symbol.psGreaterThan(upc5font) else: self.ps.lines.append("%% Skipping UPC-5 price code symbol per request") self.ps.lines=self.ps.lines + self.ps.trailer() # Can now set bounding box. self.ps.setbb() # Here we go ... if __name__ == '__main__': def printUsage(): print "Usage: bookland [-h|--help] [-v|--version] [-x|--check] [-f|--font=] [-s|--height=] [-r|--reduction=] [-o|outfile=] [-z|--quietzone] [| ]" print "Report bugs to " + MAINTAINER def printVersion(): sys.stderr.write("%s version %s %s.\n" % (MYNAME,MYVERSION,COPYRIGHT)) sys.stderr.write("Bugs to %s\n" % MAINTAINER) sys.stderr.write("This is free software and comes with NO WARRANTY\n") import getopt try: opts,args = getopt.getopt(sys.argv[1:], "xr:s:uvf:hzo:", ["reduction=","outfile=","height=","noupc", "check","version","help","font=","quietzone"]) except: printUsage() sys.exit(0) # some initial defaults: isbn = "1-56592-197-6" # Mark Lutz, "Programming Python", # O'Reilly, Sebastopol CA, 1996 price = "90000" font=None zone=None checkonly=None outfile=None heightMultiplier=1 commandLine = string.join(sys.argv) barWidthReduction = 0 # parse command line: for opt,val in opts: if opt in ("-v","--version"): printVersion() sys.exit(0) elif opt in ("-h","--help"): printUsage() sys.exit(0) elif opt in ("-f","--font"): font=val elif opt in ("-z","--quietzone"): zone=1 elif opt in ("-x","--check"): checkonly=1 elif opt in ("-s","--height"): heightMultiplier = float(val) elif opt in ("-r","--reduction"): barWidthReduction = val elif opt in ("-o","--outfile"): outfile=val if len(args)==1: isbn=args[0] elif len(args)==2: isbn=args[0] price=args[1] # Do stuff. printVersion() try: b = bookland(isbn,price,font,zone,heightMultiplier, commandLine,barWidthReduction) if not checkonly: b.ps.out(outfile) if outfile: sys.stderr.write("Output written to %s\n" % outfile) except BooklandError, message: sys.stderr.write(BooklandError + ": " + message + "\n") sys.exit(1)