[PHP] Мой штрихкод. Code128

Автор Сообщение
news_bot ®

Стаж: 6 лет 9 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
14-Июн-2021 14:35

Однажды в процессе производственной деятельности у меня появилась необходимость генерации штрихкода по стандарту code128. Появилась в виду того, что имевшаяся в эксплуатации функция (хранимая процедура в базе Oracle) генерировала клёвый, полосатый штрихкод, который читался не во всех случаях. Разработчики в своё время оттестировали эту процедуру весьма некачественно, но перерабатывать уже не собирались т.к. проект был давно сдан, а потребности в считывании так и не появились. Первая мысль — поиск готовых библиотек. Навскидку определили критерии — с pl/sql не связываемся, пусть это будет внешний сервис: возможно кусок на javascript для генерации прямо на страничке, либо обращение за картинкой к ближайшему серверу где имеется php. Беглый поиск в интернете показал что тема истоптана весьма плотно. Есть как наколенные поделки уровня лабораторки по программированию, так и мощные библиотеки для всех вариантов кодирования вплоть до qr-кодов. Варианты с JavaScript пришлось отбросить т.к. они во-первых практически все «обфусканы» (даже непонятно, то ли для сокращения объема, то ли стыдно исходники показать), во-вторых генерируют строку для отображения определенным шрифтом, наличие которого не всегда можно обеспечить на клиентском месте и требует дополнительных обработок для экранирования спецсимволов. Внимательное изучение библиотек и кусков кода на php тоже произвело тягостное впечатление — на первый взгляд всё вроде бы правильно: и классы написаны на все случаи жизни, комментарии в наличии, украшательства типа выбора цвета и рамочек, примеры подготовлены. Начнёшь вникать — хотят либо php самой распоследней версии (на боевых серверах не всегда это получается добыть), либо внутренняя логика не различима совсем, либо штрихкод на выходе получается длиннее ожидаемого. Вот последнее не дало покоя и подтолкнуло к собственной реализации.Самое время пощупать теорию. Вернее мы с ней познакомились намного раньше, просто до последнего не хотелось ввязываться в дополнительное программирование. Исторические факты опустим, а вот очень хорошее техническое описание имеется на http://code128.narod.ru/ (в архиве это файл Descript.doc ) либо в Википедии. В принципе, это всё что нам потребуется для понимания и собственной реализации алгоритма (тут я немного лукавлю — из любой готовой библиотеки нужно выдрать таблицы толщин штрихов, чтобы не вбивать их вручную). Ну и напишем всё это безобразие на php, заодно посмотрим пару прикольных моментов, про которые все забывают или стесняются использовать.Теория гласит, что code128 позволяет закодировать (сюрпрайз!) 128 символов, при этом нам доступно 3 алфавита, между которыми можно переключаться по ходу дела. Наибольший практический интерес представляют алфавит «B» для буквенно-цифровых символов и алфавит «С» который используется для кодирования цифр, но с некоторой оптимизацией — одним штриховым символом можно закодировать 2 исходных символа и получить более короткий штрихкод. Вот эта оптимизация пока не даётся ни одному php-разработчику — максимум что я видел это попытка в начале кодирования определить состав строки и при наличии только цифр переключаться на алфавит «С». В остальных библиотеках это банальная подстановка символов штрихкода по таблице.Для начала осмотрим приборы и материалы — в начале строки можем задать алфавит для кодирования, перед каждым символом можем переключить алфавит. Также мы на входе ограничены по длине строки - это очень хорошо и важно для нас. Имеем цель — получить штрихкод. Точнее, самый короткий штрихкод. Разберём пару примеров. Допустим у нас есть последовательность «ABC12DE» попробуем её закодировать разными методами, на примере изображены слева только алфавит B, справа - совместно B и С:
Наша попытка оптимизировать длину кода используя более короткий алфавит потерпела фиаско - мы получили дополнительные символы на переключениях алфавита и в результате штрихкод стал на 1 символ длиннее. Если хорошо подумать, то переключение на алфавит С выгодно при наличии 6 и более цифр подряд. Но есть ведь и граничные случаи — цифры в конце последовательности, в начале, а есть еще вариант с нечетным количеством цифр и тогда надо пристально смотреть когда переключаться на алфавит С — с первой цифры или со второй? В общем вариантов достаточно много, что в итоге у большинства отбивает желание оптимизировать длину кода, а делать всё одним алфавитом.И вот тут нас озаряет, что работа с рекурсивной функцией избавит нас от рассмотрения всех этих условий и задача станет невероятно простой — функция будет вызывать сама себя с тремя вариантами кодирования текущего символа и возвращать наиболее короткий (итоговый) вариант. Причём длина варианта включает в себя и символ переключения между алфавитами. Причин отказа от захода в ветку алгоритма совсем немного — либо кончился входной поток, либо мы не можем закодировать символ(ы) данным алфавитом (например нет 2-х цифр для алфавита «С»). Так, как входной поток имеет ограничение по длине, то дерево не будет расти бесконечно! По тексту будем реализовывать только алфавит «B» и «C» - проще для понимания и потом объясню остальное :)Сразу набросаем простейший класс который содержит сам текст, такие-же классы для рассмотрения вариантов по алфавиту B и С, длину последовательности. Делаем ему минимальный конструктор — на входе строка для кодирования, режим кодирования, ссылки на потомков. В начале конструктора добавляем проверки, чтобы не прорабатывать данную ветку дерева:
<?php
class code128 {
    private $code = '';
    private $leafB = NULL, $leafC = NULL;
    public function __construct($text, $mode = 'B')
    {
      if (strlen($text) == 0) return NULL;
      $this->mode = $mode;
      if ($mode == 'B') {
          $this->code = substr($text, 0, 1);
          $text = substr($text, 1);
      }
      else if ($mode == 'C') {
          if (strlen($text) < 2) return NULL;
          if (!is_numeric($text[0])) return NULL;
          if (!is_numeric($text[1])) return NULL;
          $this->code = substr($text, 0, 2);
          $text = substr($text, 2);
      }
      else
          return NULL;
      $this->leafB = new code128($text, 'B');
      $this->leafC = new code128($text, 'C');
    }
    public function draw()
    {
        echo "Code [" . $this->code . "]\n";
        if ($this->leafB != NULL) $this->leafB->draw();
        if ($this->leafC != NULL) $this->leafC->draw();
    }
}
    $n = new code128('s92317lsdfa4324', 'B');
    $n->draw();
?>
и сразу ловим конкретный косяк — куча "пустых" объектов. И это не смотря на то что мы явно отказались создаваться и вроде как железно возвращаем NULL! В общем сразу надо понять, что в php объект создаётся в любом случае. Полагаю что также и в остальных объектно-ориентированных языках. И все эти условия надо проверять перед созданием объекта. Следовательно правильный конструктор будет выглядеть примерно так:
public function __construct($text, $mode = 'B')
    {
      $this->mode = $mode;
      if ($mode == 'B') {
          $this->code = substr($text, 0, 1);
          $text = substr($text, 1);
      }
      else if ($mode == 'C') {
          $this->code = substr($text, 0, 2);
          $text = substr($text, 2);
      }
      if(strlen($text)>0) $this->leafB = new code128($text, 'B');
      if(strlen($text)>1)
          if(is_numeric($text[0]) && is_numeric($text[1])) $this->leafC = new code128($text, 'C');
    }
Не считая того, что мы избавились от пустых объектов, код получился даже немного короче чем прототип. Дальше начнём немного оптимизировать - добавим небольшой трюк: вместо того чтобы делать кучу проверок является ли последовательность символами, цифрами, их количество и т.д. просто смотрим в таблице наличие такого индекса в алфавите. Я уже писал что таблицу можно добыть в любой библиотеке реализующей кодирование code128? В общем напоминаю ещё раз, и готовый кусочек кода, который мне уже нравится, приведен ниже:
if($mode == 'B') list($this->code, $text) = sscanf($text, '%c%s');
  if($mode == 'C') list($this->code, $text) = sscanf($text, '%2d%s');
  if(strlen($text)>0)
      if(array_key_exists(substr($text, 0, 1), $symCode)) $this->leafB = new code128($text, 'B', $this);
  if(strlen($text)>1)
      if(array_key_exists(substr($text, 0, 2), $symCode)) $this->leafC = new code128($text, 'C', $this);
$symCode - это алфавиты, которые я загнал в файл tables.php и включаю его через require в начале исходника. Формат простой - символ алфавита => код штрихкода.
$symCode = array(
/*  alphabet B  alphabet C */
  ' ' => 0,  '00' => 0,
  '!' => 1,  '01' => 1,
  '"' => 2,  '02' => 2,
  '#' => 3,  '03' => 3,
  '$' => 4,  '04' => 4,
  '%' => 5,  '05' => 5,
  '&' => 6,  '06' => 6,
Дерево строится, но результата пока не видно. Следующим этапом необходимо определить самую короткую ветку: Первый вариант - заводим в классе счётчик и увеличиваем его при каждом переходе на ветку. Как только достигнем дна (конца исходного текста), на концах дерева будет указан размер финального кода. Второй вариант — конечные веточки ставят себе размер 1, а дальше родитель решает какой из потомков имеет код короче и ставит себе размер на 1 или 2 больше. Почему на 2? Надо учитывать накладные расходы на переключение алфавита. Кстати и в первом варианте это тоже надо учитывать. В итоге в корне дерева будет длина самой короткой последовательности. Чем хорош первый вариант? Получение итоговой последовательности практически мгновенное — возвращаешься «по папе» к корню дерева. Ну и недостаток — чтобы найти самую короткую ветку надо сделать полный обход дерева. Второй вариант — длина самой короткой последовательности известна и находится в одном месте, но получение последовательности чуток посложнее, хотя и не требует полного обхода. Попробуем проработать первый вариант — доделать надо совсем немного, просто родитель после создания потомков должен выбрать самого короткого потомка и сохранить ссылку на конечный элемент ветки. Выразился невероятно коряво, но Вы посмотрите код - там ещё страшнее :)
require 'tables.php';
class code128 {
    private $code = NULL;
    private $text = '';
    private $mode = 'Auto';
    private $len = 1;
    private $leafB = NULL, $leafC = NULL, $parent = NULL, $minCode = NULL;
    public function __construct($text, $mode = 'Auto', $parent = NULL)
    {
      global $symCode;
      $this->parent = $parent;
      $this->text = $text;
      $this->mode = $mode;
      if($parent != NULL) {
          $this->len = $this->parent->len + 1;
          if($this->parent->mode != $mode) $this->len++;
       }
      if($mode == 'B') list($this->code, $text) = sscanf($text, '%c%s');
      if($mode == 'C') list($this->code, $text) = sscanf($text, '%2d%s');
      if(strlen($text)>0)
         if(array_key_exists(substr($text, 0, 1), $symCode)) $this->leafB = new code128($text, 'B', $this);
      if(strlen($text)>1)
         if(array_key_exists(substr($text, 0, 2), $symCode)) $this->leafC = new code128($text, 'C', $this);
      if($this->leafB == NULL && $this->leafC == NULL) $this->minCode = $this;
      else {
         $this->minCode = ($this->leafB != NULL) ? $this->leafB->minCode : $this->leafC->minCode;
         if($this->leafC != NULL)
            if($this->minCode->len > $this->leafC->minCode->len) $this->minCode = $this->leafC->minCode;
      }
      return $this;
     }
Итак, худо-бедно мы нашли самый короткий путь (штрихкод). Надо его вывести для начала на экран. Делаем в два этапа: сначала от потомка возвращаемся к родителю. Итоговые символы, включая переключение между алфавитами, пушим (push) в массив. Да, в PHP есть такая функция, и она позволяет нам сделать из массива довольно удобный стэк.
private function getCode()
{
    $stack = array();
    $p = $this->minCode;
    while($p != NULL) {
        array_push($stack, $p->code);
        if($p->parent != NULL) {
            if($p->parent->mode == 'Auto') { array_push($stack, 'Start'.$p->mode); break;}
            if($p->mode != $p->parent->mode) array_push($stack, 'Code'.$p->mode);
        }
        $p = $p->parent;
    }
    return $stack;
  }
Когда потребуется вывести строку — используем array_pop. Таким образом, мы по прежнему используем данный массив как стэк и легко выводим информацию в обратном порядке. Попутно готовим дополнительную обвязку для нашего штрихкода — старт/стоп/контрольная сумма.Финал совсем близко - я уже устал писать, Вы устали читать. Предлагаю пробежаться весьма бегло. Само рисование сделано в виде SVG. Для данной задачи весьма удобно — нет необходимости кодировать размеры изображения, они будут задаваться тэгами на страничке. Кроме того, рендеринг и масштабирование осуществляется конечным устройством, что обеспечит необходимое качество в dpi соответствующее устройству вывода.
private function printPattern($code, $posX, $res, $height)
  {
    for($i = 0; $i < strlen($code); $i++) {
        $w = $res*intval($code[$i]);
        if(!($i%2))
            echo "  <rect x='$posX' y='0' width='$w' height='$height' fill='#0'/>\n";
        $posX += $w;
      }
      return $posX;
   }
   public function printSVG($resolution=1, $height=50)
   {
      global $symCode;
      global $barPattern;
      $s = $this->getCode();
      $pos = 1;
      $offset = $resolution*11;
      $width = ((count($s) + 4)*11 + 2)*$resolution;
      echo "<svg xmlns='http://www.w3.org/2000/svg' width='$width' height='$height'>\n";
      $start = $symCode[array_pop($s)];
      $checksum = $start;
      $offset = $this->printPattern($barPattern[$start], $offset, $resolution, $height);
      while(!empty($s)) {
          $code = $symCode[array_pop($s)];
          $offset = $this->printPattern($barPattern[$code], $offset, $resolution, $height);
          $checksum += $code*$pos;
          $pos++;
      }
      $offset = $this->printPattern($barPattern[$checksum%103], $offset, $resolution, $height);
      $offset = $this->printPattern($barPattern[$symCode['Stop']], $offset, $resolution, $height);
      echo "</svg>\n";
    }
Появился глобальный массив $barPattern. Искать в файле tables.php рядом с $symCode. Кусочек приведу. Там всё просто - для заданного кода выходного символа чередуются толщины черных и белых штрихов:
$barPattern = array(
  '212222',  /* 0 */
  '222122',  /* 1 */
  '222221',  /* 2 */
  '121223',  /* 3 */
  '121322',  /* 4 */
Как этим пользоваться? В файл с классом в конец добавим пару строк:
header('Content-Type: image/svg+xml');
    echo "<?xml version='1.0' encoding='UTF-8' standalone='no'?>\n\n";
    $n = new code128(html_entity_decode($_SERVER["QUERY_STRING"]));
    $n->printSVG();
попробовать можно сразу, вставив в html-страничку примерно вот такой тэг:
<img src="barcode128.php?ad32324adsFAE13413ldsFf">
Ну и напоследок. Реализованы только алфавиты «B» и «С». Уложился примерно в 100 строчек, не считая таблиц перекодировки. Реализовать алфавит «А» можно аналогичным способом просто дописав конструктор и таблицу с алфавитами, только желательно учесть один хитрый код, позволяющий кратковременно переключиться на один символ другого алфавита. Самому дописать у меня нет ни желания, ни времени, ни прочих мотиваций. (Полу)готовый проект вероятно пополнит кладбище штрихкодировщиков на гитхабе - если у кого есть желание продолжить проект - пишите, не стесняйтесь.
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_php, #_code128, #_shtrihkod (штрихкод), #_php
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 22-Ноя 20:53
Часовой пояс: UTC + 5