Floris Luiten

Close-up van een blonde jongeman met bril die en kop koffie drinkt in een cafe

Floris Luiten is programmeur bij u0192. Hij houdt van programmeren, cijfers, logica en het bestuderen van het menselijk gedrag. Op deze website laat hij zich over deze en nog meer onderwerpen uit. Wil je weten wat Floris nu denkt? Volg hem via Twitter (@florisl).
Meer informatie over Floris Luiten

EAN-13 en PHP

Wat doe je als je veel vrije tijd hebt en geen zin hebt om voor de televisie te hangen? Dan ga je programmeren natuurlijk!

In mijn "PHP carrière" heb ik veel verschillende dingen gemaakt, zoals een webscraper, sudoku-solver en een IRC client. Dit zijn meestal resultaten geweest van tijdverdrijf (ook wel "hobby" genoemd) en leverden niet echt iets op waar je wat mee kunt doen. Dat moet anders kunnen, nietwaar?

Zodoende besloot ik om bij m'n volgende keer dat ik dit uitoefende – hoe heette dat ook alweer, hobby?– iets te maken waar je echt wat aan hebt. Het resultaat beschrijf ik hier.

EAN kun je dat eten?

Als je om je heen kijkt zie je altijd interessante dingen, zo viel mijn oog op de streepjescode van {sluipreclame hier}. Ik dacht, zo moelijk kan dat toch niet zijn om je eigen streepjescode te genereren vanuit een reeks cijfers? Nee, het is zeer makkelijk eigenlijk!

Ik was reeds op de hoogte dat er veel verschillende soorten barcodes zijn, waarbij EAN13 de meest gebruikte is in de supermarkten. Derhalve besloot ik om hiermee aan de slag te gaan.

First things first

Aangezien er verschillende soorten barcodes zijn en ik vrij veel vrije tijd heb, bedacht ik dat ik in de toekomst wellicht ook met andere types barcode aan de slag zou gaan. Om het later gemakkelijk in één library te zetten besloot ik om te beginnen met een abstract class waar de individuele types barcodes vanuit extenden. In een bestandstructuur kun je dit zien als de master, waarbij alle andere klasses uitgaan van dit zgn "skelet".

<?php
abstract class digitBarCodes {
  abstract public function 
calculateChecksum($code);
  
  abstract public function 
getBitarray($code);

  public function 
drawBarcode($something) {
    
/* Deze functie moet nog geschreven worden, maar zal later een afbeelding
       genereren en returnen. */
  
}
}

In deze superclass zullen verder alle eventuele ondersteunende functies komen die gebruikt worden in door de verschillende barcode types. Uitleg over de getBitarray-functie volgt, maar deze zal later gebruikt worden om de daadwerkelijke afbeelding te kunnen genereren.
Je ziet bovendien de drawBarcode functie, deze kan door de verschillende type barcodes gebruikt worden om een afbeelding te laten genereren. Aangezien ik nu nog niet weet aan de hand van welke variabelen dit zal zijn, heb ik de placeholder something gebruikt. Dit zullen we later aanpassen.

Genoeg voorwerk gedaan, laten we aan de slag gaan!

Check! Sum? kun je dat eten?

Laten we beginnen met het berekenen van de checksum. Dit getal dient als controlemiddel zodat een scanner (bij de kass bijvoorbeeld) weet dat de gescande code inderdaad klopt - en niet dat hij 'n "foutje" heeft gemaakt. Het kan namelijk in de "echte wereld" voorkomen dat een barcode beschadigt is geraakt, daarom bestaat zo'n checksum code.

De checksum methode van EAN13 in het geval van een getal met een even lengte werkt als volgt:

  1. Van alle oneven getallen word de som genomen
  2. Bij alle even getallen word het getal eerst vermenigvuldigd met 3 en daar dan de som van genomen
  3. De twee sommen worden bij elkaar opgeteld
  4. Bereken modulo 10 van deze som (noemen we X)
  5. 10 - X en je hebt de checksum

Als het getal een oneven lengte heeft (wat in EAN13 niet voorkomt, maar wel in EAN8) worden niet de even getallen vermenigvuldigd met 3, maar de oneven getallen.

Neem als voorbeeld het getal 400638133393:

  1. Bereken de som van de oneven getallen (4 + 0 + 3 + 1 + 3 + 9 = 20)
  2. Bereken de som van elk even getal vermenigvuldigd met 3 ((3 * 0) + (3 * 6) + (3 * 8) + (3 * 3) + (3 * 3) + (3 * 3) = 69)
  3. 20 + 69 = 89
  4. Modulo 10 van 89 = 9
  5. 10 - 9 = 1

Als je 't nog niet helemaal snapt, kijk nog even naar de uitleg op de eerder genoemde Wikipedia pagina.

And now for something completely the same

De hierboven beschreven methode werkt op papier erg gemakkelijk, maar in een script is dit niet optimaal. Je kijkt eerst hoe lang de input is, deelt daarop het getal in verschillende groepjes, voert op elk groepje een eigen berekening uit, telt de groepjes bij elkaar op en voert dan de modulo uit. Hoewel je dit letterlijk kunt implementeren, is er een handigere (en snellere?) methode om dit te doen.

We weten dat het laatste getal vermenigvuldigt zal worden met 3, het getal daarvoor met 1, het getal daarvoor met 3 et cetera. Waarom beginnen we daarom niet simpelweg achteraan en werken we vanuit daar naar voren? Bovendien hoeven we niet 2 sommen uit te rekenen, maar kunnen we simpelweg één variabele bijhouden met de som van zowel de even getallen, als de oneven.

Laten we het daarom even anders opschrijven, hoe we 't beter kunnen aanpakken in ons te schrijven script:

  1. Initialiseer een variabele som
  2. Neem het getal en draai deze om (dwz. 400638133393 word 393331836004)
  3. Kijk of het getal even of oneven is
    • Is het getal oneven? Vermenigvuldig het getal met 3 en tel op bij de som
    • Is het getal even? Tel het getal op bij de som
  4. Voer modulo 10 uit
  5. 10 - modulo

Hoewel het nog steeds 5 stappen zijn op papier,  is dit veel gemakkelijker uit te voeren! Voorbeeld in PHP:

<?php
function calculateChecksum($code) {
  
$sum 0;
  foreach (
str_split(strrev($code)) AS $index => $digit) {
    
//Thank god PHP thinks 1 & 0 == 0
    
$sum += ($index) ? $digit $digit 3;
  }
  return (
$sum 10 == 0) ? 10 - ($sum 10);
}

De strrev-functie draait het getal om (alsof het een string is, dank PHP voor weak typing!), waarbij de str_split-functie de string opdeelt in losse (een array van) cijfers. De foreach-loop itereert vervolgens over deze cijfers. De som word in elke iteratie verhoogd met het cijfer, of het cijfer * 3, afhankelijk of het cijfer even danwel oneven is.

Voor de oplettende lezers: In bovenstaande functie doen we het eigelijk nét iets anders dan beloofd met even/oneven. Dit komt komt omdat PHP een 0-index kent. Hierdoor staat het laatste getal in de oorspronkelijke code op de 0'ste plaats en het op-één-na-laatste op de 1'ste plaats.

Verder vind er in de laatste regel nog een extra controle plaats. Als de som bijvoorbeeld 90 is, zal 10 % 10 gelijk zijn aan 0. In dat geval moeten we simpelweg 0 gebruiken, in alle andere gevallen vind 10 - modulo plaats (zoals hierboven beschreven in stap 5).

And now for something completely different

Laten we aan de slag gaan met het encoderen van de code. Deze stap is nodig om de code om te zetten naar "iets" wat gebruikt kan worden om de daadwerkelijke afbeelding te maken. De encodering van de EAN13 gaat als volgt:

Uit deze uitleg kun je al opmaken dat het eerste getal niet in de barcode zelf voorkomt, dit heeft te maken met compatibiliteit met UPC. Voor een exacte uitleg hoe de encodering plaatsvindt, lees de tweemaal eerder genoemde Wikipedia pagina.

We maken nu de functie om de mask op te vragen aan de hand van de code:

<?php
function getStructureMask($code) {
  if (!
ctype_digit($code)) {
    return 
false;
  }
  switch (
substr($code01)) {
    case 
'0':
      return 
'LLLLLLRRRRRR';
    case 
'1':
      return 
'LLGLGGRRRRRR';
    case 
'2':
      return 
'LLGGLGRRRRRR';
    case 
'3':
      return 
'LLGGGLRRRRRR';
    case 
'4':
      return 
'LGLLGGRRRRRR';
    case 
'5':
      return 
'LGGLLGRRRRRR';
    case 
'6':
      return 
'LGGGLLRRRRRR';
    case 
'7':
      return 
'LGLGLGRRRRRR';
    case 
'8':
      return 
'LGLGGLRRRRRR';
    case 
'9':
      return 
'LGGLGLRRRRRR';
  }
  return 
false//Even though we can never actually reach this
}

We hadden hier een lookuptabel kunnen gebruiken, maar aangezien je deze functie slechts éénmaal aanroept heb ik gekozen voor een switch oplossing. Aan de hand van het eerste getal van de code geeft de functie een string terug waarmee de individuele cijfers geëncodeert kunnen worden. Zoals je ziet gebruik ik dezelfde benamingen zoals in de wikipedia pagina.

Zoals op de wikipedia pagina staat is R afgeleid van L en G van R. Hierdoor hoeven we alleen de L te implementeren (d.m.v een lookuptable, wat eigenlijk een array is). Daarna kunnen we simpelweg R en G berekenen.

Het jammere is dat je in PHP niet direct gebruik kunt maken van binary getallen, er zijn derhalve twee oplossingen:

  1. Gebruik een string ("01010")
  2. Gebruik een array met integers (array(0, 1, 0, 1, 0))

Aangezien het niet handig is om met een string te rekenen, zou je al snel een string constant omzetten naar array/string/array/string. Daarom besloot ik om te werken met een array met integers welke ik vanaf nu een bitArray noem.

Net ontdekt dat binary integers vanaf PHP 5.4.0 is ondersteund, joeppie!

<?php
$codeLookupTable 
= array(
  
'L' => array(
    
'0' => 
      array(
0001101),
    
'1' => 
      array(
0011001),
    
'2' => 
      array(
0010011),
    
'3' => 
      array(
0111101),
    
'4' => 
      array(
0100011),
    
'5' => 
      array(
0110001),
    
'6' => 
      array(
0101111),
    
'7' => 
      array(
0111011),
    
'8' => 
      array(
0110111),
    
'9' => 
      array(
0001011)
  )
);

function 
encode($integer$codeBase) {
  if (
$codeBase == 'R') {
    return 
bitArrayNOT(codeLookupTable['L'][$integer]);
  } elseif (
$codeBase == 'G') {
    return 
array_reverse(bitArrayNOT(codeLookupTable['L'][$integer]));
  } else {
    return 
$codeLookupTable['L'][$integer];
  }
}

De lookuptabel is overgenomen van de wikipedia pagina en spreekt voor zich neem ik aan. In de encode functie kijk ik simpelweg wat het getal is en in welke codeBase dit staat. Ik maak nu gebruik van een nog niet gedefineerde functie bitArrayNOT. Deze functie doet een simpele bitwise NOT-functie uitvoeren op een getal. Concreet betekent dit dat een 0 een 1 word, een 1 een 0 word. Deze functie staat overigens in de abstractclass zodat andere barcodes deze functie ook kunnen gebruiken:

<?php
abstract class digitBarCodes {
  
/* De andere functies staan hier nog steeds */
  
public function bitArrayNOT($bits) {
    foreach (
$bits AS $index => $bit) {
      
$bits[$index] = $bit;
    }
    return 
$bits;
  }
}

Joining them up

Zo, dit alles bij elkaar begint al een aardig geheel te worden. Om alles bij elkaar te brengen gaan we een overkoepelende functie maken die aan de hand van een code een bitArray teruggeeft. Deze bitArray kunnen we later aan een plot-functie geven die de code daadwerkelijk omzet naar een barcode.

<?php
function getBitarray($code) {
  if (!
ctype_digit($code) OR !ctype_digit($code)) {
    return 
false;
  }
  if (
strlen($code) == BARCODE_LENGTH_NO_CHECKSUM) {
    
$code $code calculateChecksum($code);
  } elseif (
strlen($code) == (BARCODE_LENGTH_NO_CHECKSUM BARCODE_CHECKSUM_LENGTH)) {
    if (
calculateChecksum(substr($code0, (BARCODE_CHECKSUM_LENGTH))) != substr($code, (BARCODE_CHECKSUM_LENGTH))) {
      return 
false;
    }
  } else {
    return 
false;
  }
  
  
$return = array(array(101)); // Left guard
  
$structureMask str_split(getStructureMask($code));
  
  foreach (
$structureMask AS $index => $digit) {
    
$return[] = encode($digit$structureMask[$index]);
    if (
$index == 5) {
      
$return[] = array(01010); // Center guard
    
}
  }
  
$return[] = array(101); // Right guard
  
return $return;
}

Show me that image!

De laatste stap die we nog moeten doen is het daadwerkelijk genereren van een afbeelding. Dit gaat verrassend makkelijk aangezien we 't grootste reeds gedaan hebben. De functie die ik beschrijf zullen we opnemen in de abstract-class zodat deze voor alle subclasses beschikbaar is.

Aan de hand van de bitArray die we genereren in de getBitarray-functie kunnen we heel simpel de daadwerkelijke barcode genereren. We hoeven enkel over de bitArray te itereren. Als we een 0 zien doen we niets, zien we een 1 dan zetten we een zwart lijntje. Dan schuiven we één pixel naar rechts en herhalen we dit. We weten niet op welke resolutie de afbeelding getoond worden, dus laten we een functie maken die zowel kleine als grote barcodes kan genereren.

Om een afbeelding met variabele breedte te maken moeten we eerst tellen hoeveel streepjes we zullen zetten. Een simpele count over de bitArray werkt niet omdat we werken met een multi-dimensionale array. De oplossing?

<?php
$imageWidth 
count($bitArrayCOUNT_RECURSIVE) - count($bitArray);

Voila!

Verder gebruik ik de functie imagelinthink zoals gevonden op in de PHP manual. De rest van de code spreekt voor zich:

<?php
function generateBarcodeImage($bitArray$size=1) {
  
$imageWidth count($bitArrayCOUNT_RECURSIVE) - count($bitArray);
  
$image imagecreate(($imageWidth $size), (80 $size));
  
imagecolorallocate($image255255255); // White background
  
$imColorBarcode imagecolorallocate($image000); // Black color
  
$xPos 0;
  foreach (
$bitArray AS $bitArray) {
    foreach (
$bitArray AS $bit) {
        if (
$bit == 1) {
          
imagelinethick($image$xPos0$xPos, (80 $size), $imColorBarcode$size);
        }
        
$xPos += $size;
    }
  }
  return 
$image;
}

The result

Uiteraard laten we dit geen losse functies, maar hebben we dit netjes in één class gezet. Het hele resultaat tot zover is als volgt:

<?php
abstract class digitBarCodes {
  
/**
   * Dummy function for returning the checksum based on the
   * code. Returns false if no checksum could be generated
   *
   * @param integer The code
   * @return integer/False The checksum as digit or False
   *                       when no checksum could be created
   */
  
abstract public function calculateChecksum($code);
  
  
/**
   * Dummy function to generate a bitArray from the code.
   * This bitArray can be used to render the actual
   * barcode as an image.
   * Returns False when no bitArray could be generated, 
   * for example when the checksum failed or non-valid
   * characters were found.
   *
   * @param string The code with or without the checksum
   * @param array/False A bitArray or False otherwise
   */
  
abstract public function getBitarray($code);
  
  
/**
   * Dummy function to return the code if it's valid,
   * if no checksum is provided the return will include this
   *
   * @param string The code to check
   * @param string/False The valid code or False otherwise
   */
  
abstract public function getValidatedCode($code);
  
  
/**
   * Bitwise NOT on a bitArray (an array containing 0 or 1's)
   *
   * @param array The bitArray
   * @return array The NOT bitArray
   */
  
public function bitArrayNOT($bits) {
    foreach (
$bits AS $index => $bit) {
      
$bits[$index] = $bit;
    }
    return 
$bits;
  }
  
  
/**
   * Convert a bitArray to an integer
   *
   * @param array The bitarray
   * @return integer The integer
   */
  
public function bitArrayToInteger($bits) {
    
$bitValue 1;
    
$sum 0;
    
    foreach (
array_reverse($bits) AS $index => $bit) {
      
$sum += $bit $bitValue;
      
$bitValue *= 2;
    }
    return 
$sum;
  }
  
  public function 
getBarcodeImage($code$size=1) {
    
$code $this->getValidatedCode($code);
    if (
$code == false) {
      return 
false;
    }
    return 
$this->generateBarcodeImage($this->getBitarray($code), $size);
  }
  
/**
   * Generates an image containing the barcode to use
   * in printing or on screen. 
   *
   * @param array The bitarray containing the code
   */
  
public function generateBarcodeImage($bitArray$size=1) {
    
$imageWidth count($bitArrayCOUNT_RECURSIVE) - count($bitArray);
    
$image imagecreate(($imageWidth $size), (80 $size));
    
imagecolorallocate($image255255255); // White background
    
$imColorBarcode imagecolorallocate($image000); // Black color
    
$xPos 0;
    foreach (
$bitArray AS $bitArray) {
      foreach (
$bitArray AS $bit) {
        if (
$bit == 1) {
          
$this->imagelinethick($image$xPos0$xPos, (80 $size), $imColorBarcode$size);
        }
        
$xPos += $size;
      }
    }
    return 
$image;
  }
  
  
/**
   * Draw a line on the image of a specific width.
   * 
   * @link http://nl.php.net/manual/en/function.imageline.php
   * 
   * @param resource The image resource
   * @param integer The x1
   * @param integer The y1
   * @param integer The x2
   * @param integer The y2
   * @param resource The color
   * @param integer The thickness of the line
   * @return boolean True on success, FALSE otherwise
   */
  
private function imagelinethick($image$x1$y1$x2$y2$color$thick 1) {
    if (
$thick == 1) {
      return 
imageline($image$x1$y1$x2$y2$color);
    }
    
$t $thick 0.5;
    if (
$x1 == $x2 || $y1 == $y2) {
      return 
imagefilledrectangle($imageround(min($x1$x2) - $t), round(min($y1$y2) - $t), round(max($x1$x2) + $t), round(max($y1$y2) + $t), $color);
    }
    
$k = ($y2 $y1) / ($x2 $x1);
    
$a $t sqrt(pow($k2));
    
$points = array(
      
round($x1 - (1+$k)*$a), round($y1 + (1-$k)*$a),
      
round($x1 - (1-$k)*$a), round($y1 - (1+$k)*$a),
      
round($x2 + (1+$k)*$a), round($y2 - (1-$k)*$a),
      
round($x2 + (1-$k)*$a), round($y2 + (1+$k)*$a),
    );
    
imagefilledpolygon($image$points4$color);
    return 
imagepolygon($image$points4$color);
  }
}
class 
EAN13Barcode extends digitBarCodes {
  const 
BARCODE_LENGTH_NO_CHECKSUM 12;
  const 
BARCODE_CHECKSUM_LENGTH 1;
  
  private 
$codeLookupTable = array(
    
'L' => array(
      
'0' => 
        array(
0001101),
      
'1' => 
        array(
0011001),
      
'2' => 
        array(
0010011),
      
'3' => 
        array(
0111101),
      
'4' => 
        array(
0100011),
      
'5' => 
        array(
0110001),
      
'6' => 
        array(
0101111),
      
'7' => 
        array(
0111011),
      
'8' => 
        array(
0110111),
      
'9' => 
        array(
0001011)
      ));
  
  
/**
   * Function for returning the checksum based on the input.
   * Returns false if no checksum could be generated
   *
   * @param string The barcode
   * @return integer The checksum as digit
   */
  
public function calculateChecksum($code) {
    if (!
ctype_digit($code) OR !ctype_digit($code)) {
      return 
false;
    }
    
$sum 0;
    foreach (
str_split(strrev($code)) AS $index => $digit) {
      
//Thank god PHP thinks 1 & 0 == 0
      
$sum += ($index) ? $digit $digit 3;
    }
    return (
$sum 10 == 0) ? 10 - ($sum 10);
  }
  
  
/**
   * Function to generate a bitArray from the code.
   * This bitArray can be used to render the actual
   * barcode as an image.
   *
   * @param string The code with or without the checksum
   * @param array/False A bitArray or False otherwise
   */
  
public function getBitarray($code) {
    
$code $this->getValidatedCode($code);
    if (
$code == false) {
      return 
false;
    }
        
    
$return = array(array(101)); // Left guard
    
$structureMask str_split($this->getStructureMask($code));
    
    foreach (
str_split(substr($code1)) AS $index => $digit) {
      
$return[] = $this->encode($digit$structureMask[$index]);
      if (
$index == 5) {
        
$return[] = array(01010); // Center guard
      
}
    }
    
$return[] = array(101); // Right guard
    
return $return;
  }
  
  
/**
   * Function to return the code if it's valid,
   * if no checksum is provided the return will include this
   *
   * @param string The code to check
   * @param string/False The valid code or False otherwise
   */
  
public function getValidatedCode($code) {
    if (!
ctype_digit($code) OR !ctype_digit($code)) {
      return 
false;
    }
    if (
strlen($code) == self::BARCODE_LENGTH_NO_CHECKSUM) {
      return 
$code $this->calculateChecksum($code);
    } elseif (
strlen($code) == (self::BARCODE_LENGTH_NO_CHECKSUM self::BARCODE_CHECKSUM_LENGTH)) {
      if (
$this->calculateChecksum(substr($code0, (self::BARCODE_CHECKSUM_LENGTH))) != substr($code, (self::BARCODE_CHECKSUM_LENGTH))) {
        return 
false;
      }
      return 
$code;
    }
    return 
false;
  }
  
  
/**
   * Returns the mask to use for the EAN13 code
   *
   * @param string The code (with or without the checksum)
   * @return string The mask to use
   */
  
private function getStructureMask($code) {
    if (!
ctype_digit($code)) {
      return 
false;
    }
    switch (
substr($code01)) {
      case 
'0':
        return 
'LLLLLLRRRRRR';
      case 
'1':
        return 
'LLGLGGRRRRRR';
      case 
'2':
        return 
'LLGGLGRRRRRR';
      case 
'3':
        return 
'LLGGGLRRRRRR';
      case 
'4':
        return 
'LGLLGGRRRRRR';
      case 
'5':
        return 
'LGGLLGRRRRRR';
      case 
'6':
        return 
'LGGGLLRRRRRR';
      case 
'7':
        return 
'LGLGLGRRRRRR';
      case 
'8':
        return 
'LGLGGLRRRRRR';
      case 
'9':
        return 
'LGGLGLRRRRRR';
    }
    return 
false//Even though we can never actually reach this
  
}
  
  
/**
   * Returns the L/G/R-code for the integer for usage with EAN13
   *
   * @param integer The integer to encode
   * @param string The codebase (R/G/L)
   * @return integer The encoded integer
   */
  
private function encode($integer$codeBase) {
    if (
$codeBase == 'R') {
      return 
$this->bitArrayNOT($this->codeLookupTable['L'][$integer]);
    } elseif (
$codeBase == 'G') {
      return 
array_reverse($this->bitArrayNOT($this->codeLookupTable['L'][$integer]));
    } else {
      return 
$this->codeLookupTable['L'][$integer];
    }
  }
}

Dat is 't, de hele class netjes opgemaakt en klaar voor gebruik. Als je wilt kun je deze EAN-13 class downloaden. Als je deze class gebruikt in je eigen werk zou een bedankje erg welkom zijn!

Ja, ik weet dat dit reeds bestaat in de PEAR, maar zelf maken is veel leuker en leerzamer bovendien!

Bekijk andere blog posts