Initial commit

This commit is contained in:
Chris 2017-10-26 00:42:37 +02:00
commit a410ce8b19
11 changed files with 602 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/composer.lock
/vendor

30
composer.json Normal file
View File

@ -0,0 +1,30 @@
{
"name": "noccylabs/gd-label",
"description": "Generate and print labels",
"type": "library",
"license": "LGPL-3.0",
"authors": [
{
"name": "Christopher Vagnetoft",
"email": "cvagnetoft@gmail.com"
}
],
"require": {
"php": ">=7.0",
"ext-gd": "*",
"ext-xml": "*",
"noccylabs/csslike": "^0.1.1",
"picqer/php-barcode-generator": "^0.2.2"
},
"autoload": {
"psr-4": {
"NoccyLabs\\GdLabel\\": "src/"
}
},
"repositories": [
{
"type": "composer",
"url": "https://packages.noccylabs.info/composer/"
}
]
}

94
src/Element.php Normal file
View File

@ -0,0 +1,94 @@
<?php
namespace NoccyLabs\GdLabel;
abstract class Element
{
/** @var Element[] $children */
protected $children = [];
/** @var array $props */
protected $props = [];
/**
* Add a new child
*
* @param Element $element
*/
public function addChild(Element $element)
{
$this->children[] = $element;
}
/**
* Get all children
*
* @return Element[] Child elements
*/
public function getChildren():iterable
{
return $this->children;
}
/**
* Render the element
*
* @param gd $canvas
* @param int $x
* @param int $y
* @param int $width
* @param int $height
* @param array $params
*/
public function drawElement($canvas, int $x, int $y, int $width, int $height, array $params)
{
$this->draw($canvas, $x, $y, $width, $height, $params);
}
protected function draw($canvas, int $x, int $y, int $width, int $height, array $params)
{
}
protected function rgb($color)
{
if (is_integer($color)) {
return $color;
} elseif (preg_match('/^#[0-9A-F]{6}$/i', $color)) {
[$r,$g,$b] = \sscanf($color, "#%02x%02x%02x");
return $r | $g << 8 | $b << 16;
} else {
return null;
}
}
public function setProp(string $key, $value)
{
$this->props[$key] = $value;
}
public function getProp(string $key, $default=null)
{
return array_key_exists($key, $this->props)
?$this->props[$key]
:$default
;
}
public function getPropRgb(string $key, $default=null)
{
$color = $this->getProp($key, $default);
return $this->rgb($color);
}
public function getProps():iterable
{
return $this->props;
}
protected function warning($msg)
{
defined("STDERR") && fprintf(STDERR, "%s\n", rtrim($msg));
}
}

View File

@ -0,0 +1,98 @@
<?php
namespace NoccyLabs\GdLabel\Elements;
use NoccyLabs\GdLabel\Element;
use Picqer\Barcode\BarcodeGeneratorPNG;
class BarcodeElement extends Element
{
protected $image;
protected function draw($gd, int $x, int $y, int $width, int $height, array $params)
{
$pw = $this->getProp("width", $width);
$ph = $this->getProp("height", $height);
if (!$this->image) {
$this->renderBarcode($params);
}
$sx = imagesx($this->image);
$sy = imagesy($this->image);
$aspect = $sx/$sy;
if ($aspect>1) {
$dw = $width;
$dh = $width / $aspect;
} else {
$dw = $height * $aspect;
$dh = $height;
// printf("--%d--\n", $dw);
}
imagecopyresampled($gd, $this->image, $x, $y, 0, 0, $dw, $dh, $sx, $sy);
}
protected function renderBarcode(array $params)
{
if (($bound = $this->getProp("bind-value"))) {
$value = array_key_exists($bound, $params)?$params[$bound]:"?{$bound}?";
} else {
$value = $this->getProp("value");
}
$color = $this->getPropRgb("text-color");
$type = $this->getPropType("barcode-type");
$barcodeGenerator = new BarcodeGeneratorPNG();
$barcodeData = $barcodeGenerator->getBarcode($value, $type, 2, 30, [0,0,0]);
$barcode = imagecreatefromstring($barcodeData);
$this->image = $barcode;
}
protected function getPropType(string $name)
{
$typeString = $this->getProp($name);
switch ($typeString) {
case 'code39': $type = BarcodeGeneratorPNG::TYPE_CODE_39; break;
case 'code39-c': $type = BarcodeGeneratorPNG::TYPE_CODE_39_CHECKSUM; break;
case 'code39-e': $type = BarcodeGeneratorPNG::TYPE_CODE_39E; break;
case 'code39-ec': $type = BarcodeGeneratorPNG::TYPE_CODE_39E_CHECKSUM; break;
case 'code93': $type = BarcodeGeneratorPNG::TYPE_CODE_93; break;
case 'standard25': $type = BarcodeGeneratorPNG::TYPE_STANDARD_2_5; break;
case 'standard25-c': $type = BarcodeGeneratorPNG::TYPE_STANDARD_2_5_CHECKSUM; break;
case 'interleaved25': $type = BarcodeGeneratorPNG::TYPE_INTERLEAVED_2_5; break;
case 'interleaved25-c': $type = BarcodeGeneratorPNG::TYPE_INTERLEAVED_2_5_CHECKSUM; break;
case 'code128': $type = BarcodeGeneratorPNG::TYPE_CODE_128; break;
case 'code128a': $type = BarcodeGeneratorPNG::TYPE_CODE_128_A; break;
case 'code128b': $type = BarcodeGeneratorPNG::TYPE_CODE_128_B; break;
case 'code128c': $type = BarcodeGeneratorPNG::TYPE_CODE_128_C; break;
case 'ean2': $type = BarcodeGeneratorPNG::TYPE_EAN_2; break;
case 'ean5': $type = BarcodeGeneratorPNG::TYPE_EAN_5; break;
case 'ean8': $type = BarcodeGeneratorPNG::TYPE_EAN_8; break;
case 'ean13': $type = BarcodeGeneratorPNG::TYPE_EAN_13; break;
case 'upca': $type = BarcodeGeneratorPNG::TYPE_UPC_A; break;
case 'upce': $type = BarcodeGeneratorPNG::TYPE_UPC_E; break;
case 'msi': $type = BarcodeGeneratorPNG::TYPE_MSI; break;
case 'msi-c': $type = BarcodeGeneratorPNG::TYPE_MSI_CHECKSUM; break;
case 'postnet': $type = BarcodeGeneratorPNG::TYPE_POSTNET; break;
case 'planet': $type = BarcodeGeneratorPNG::TYPE_PLANET; break;
case 'rms4cc': $type = BarcodeGeneratorPNG::TYPE_RMS4CC; break;
case 'kix': $type = BarcodeGeneratorPNG::TYPE_KIX; break;
case 'imb': $type = BarcodeGeneratorPNG::TYPE_IMB; break;
case 'codabar': $type = BarcodeGeneratorPNG::TYPE_CODABAR; break;
case 'code11': $type = BarcodeGeneratorPNG::TYPE_CODE_11; break;
case 'pharma': $type = BarcodeGeneratorPNG::TYPE_PHARMA_CODE; break;
case 'pharma-2': $type = BarcodeGeneratorPNG::TYPE_PHARMA_CODE_TWO_TRACKS; break;
default: $type = BarcodeGeneratorPNG::TYPE_CODE_39; break;
}
return $type;
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace NoccyLabs\GdLabel\Elements;
use NoccyLabs\GdLabel\Element;
class FrameElement extends Element
{
protected function draw($gd, int $x, int $y, int $width, int $height, array $params)
{
$color = $this->getPropRgb("line-color");
if ($color === null) {
return;
}
$radius = (int)$this->getProp("border-radius", 0);
$thickness = (int)$this->getProp("line-width", 1);
\imagesetthickness($gd, $thickness);
if ($radius == 0) {
\imagerectangle($gd, $x, $y, $x + $width, $y + $height, $color);
} else {
\imageline($gd, $x + $radius, $y, $x + $width - $radius, $y, $color);
\imageline($gd, $x + $radius, $y + $height, $x + $width - $radius, $y + $height, $color);
\imageline($gd, $x, $y + $radius, $x, $y + $height - $radius, $color);
\imageline($gd, $x + $width, $y + $radius, $x + $width, $y + $height - $radius, $color);
\imagearc($gd, $x + $radius, $y + $radius, $radius * 2, $radius * 2, 180, 270, $color);
\imagearc($gd, $x + $radius, $y + $height - $radius, $radius * 2, $radius * 2, 90, 180, $color);
\imagearc($gd, $x + $width - $radius, $y + $radius, $radius * 2, $radius * 2, 270, 0, $color);
\imagearc($gd, $x + $width - $radius, $y + $height - $radius, $radius * 2, $radius * 2, 0, 90, $color);
}
\imagesetthickness($gd, 1);
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace NoccyLabs\GdLabel\Elements;
use NoccyLabs\GdLabel\Element;
class ImageElement extends Element
{
protected $image;
protected function draw($gd, int $x, int $y, int $width, int $height, array $params)
{
$src = $this->getProp("src");
if (!$src || !\file_exists($src)) {
return;
}
if (!$this->image) {
$data = file_get_contents($src);
$this->image = imagecreatefromstring($data);
}
$sx = imagesx($this->image);
$sy = imagesy($this->image);
imagecopyresampled($gd, $this->image, $x, $y, 0, 0, $width, $height, $sx, $sy);
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace NoccyLabs\GdLabel\Elements;
use NoccyLabs\GdLabel\Element;
class StringElement extends Element
{
protected function draw($gd, int $x, int $y, int $width, int $height, array $params)
{
if (($bound = $this->getProp("bind-value"))) {
$value = array_key_exists($bound, $params)?$params[$bound]:"?{$bound}?";
} else {
$value = $this->getProp("value");
}
$value = \preg_replace_callback('/([åäöÅÄÖ]{1})/', function ($c) {
switch ($c) {
case 'å':
case 'ä':
return "a";
case 'ö':
return "o";
case 'Å':
case 'Ä':
return "A";
case 'Ö':
return "O";
}
}, $value);
$color = $this->getPropRgb("text-color");
$size = $this->getProp("text-size", 0) * 2;
$weight = $this->getProp("font-weight");
if ($weight == "bold") {
$size++;
}
imagestring($gd, $size, $x, $y, $value, $color);
}
}

View File

@ -0,0 +1,72 @@
<?php
namespace NoccyLabs\GdLabel\Elements;
use NoccyLabs\GdLabel\Element;
class TextElement extends Element
{
private static $fontCache = [];
private static function findFont($font)
{
if (count(self::$fontCache)==0) {
self::updateFonts();
}
$font = strtolower($font);
if (array_key_exists($font, self::$fontCache)) {
return self::$fontCache[$font];
}
return false;
}
private static function updateFonts()
{
exec("fc-list", $output, $ret);
if ($ret != 0) {
fprintf(STDERR, "%s\n", join("\n",$output));
return;
}
foreach ($output as $line) {
$seg = explode(":", $line);
$file = $seg[0];
$name = strtolower(trim(preg_replace("/[^A-Za-z0-9 ]/", '', $seg[1])));
self::$fontCache[$name] = $file;
}
}
protected function draw($gd, int $x, int $y, int $width, int $height, array $params)
{
if (($bound = $this->getProp("bind-value"))) {
$value = array_key_exists($bound, $params)?$params[$bound]:"?{$bound}?";
} else {
$value = $this->getProp("value");
}
$font = self::findFont("liberation sans");
if (!$font) {
return;
}
$color = $this->getPropRgb("text-color");
$size = $this->getProp("font-size", 8);
$dims = imagettfbbox($size, 0, $font, $value);
$textAscent = abs($dims[7]);
$textDescent = abs($dims[1]);
$textWidth = abs($dims[0])+abs($dims[2]);
$textHeight = $textAscent+$textDescent;
imagettftext($gd, $size, 0, $x, $y + $textAscent, $color, $font, $value);
//imagestring($gd, $size, $x, $y, $value, $color);
}
}

17
src/Label.php Normal file
View File

@ -0,0 +1,17 @@
<?php
namespace NoccyLabs\GdLabel;
class Label extends Element
{
protected function draw($gd, int $x, int $y, int $width, int $height, array $params)
{
if ($bg = $this->getPropRgb("background-color")) {
\imagefilledrectangle($gd, $x, $y, $x + $width, $y + $height, $bg);
}
}
}

98
src/Loader/XmlLoader.php Normal file
View File

@ -0,0 +1,98 @@
<?php
namespace NoccyLabs\GdLabel\Loader;
use NoccyLabs\CssLike\Stylesheet;
use NoccyLabs\GdLabel\Label;
use SimpleXMLElement;
use NoccyLabs\GdLabel\Element;
class XmlLoader
{
public function loadFile($filename)
{
$xml = simplexml_load_file($filename);
$label = new Label();
$styles = new Stylesheet();
foreach ($xml->style as $style) {
if (($src = (string)@$style['src'])) {
$ss = file_get_contents($src);
} else {
$ss = (string)$style;
}
$ssb = new Stylesheet();
$ssb->parseString($ss);
$styles->appendStylesheet($ssb);
}
$rules = $styles->select("label");
foreach ($rules as $prop=>$value) {
$label->setProp($prop, $value);
}
$this->parseAttributes($xml, $label, $styles);
foreach ($xml->children() as $child) {
if ($child->getName() == "style") {
continue;
}
$this->parseElement($child, $label, $styles);
}
return $label;
}
private function parseElement(SimpleXmlElement $xml, Element $elem, Stylesheet $style)
{
$name = $xml->getName();
$fqcn = "NoccyLabs\\GdLabel\\Elements\\".ucwords($name)."Element";
if (!class_exists($fqcn)) {
throw new \RuntimeException("Invalid element {$name} (expected {$fqcn})");
}
$obj = new $fqcn();
$rules = $style->select($name);
foreach ($rules as $prop=>$value) {
$elem->setProp($prop, $value);
}
$this->parseAttributes($xml, $obj, $style);
foreach ($xml->children() as $child) {
$this->parseElement($child, $obj, $style);
}
$elem->addChild($obj);
}
private function parseAttributes(SimpleXmlElement $xml, Element $elem, Stylesheet $style)
{
$props = [];
foreach ($xml->attributes() as $attr) {
$name = $this->camelToDash($attr->getName());
$value = (string)$attr;
$props[$name] = $value;
}
if (array_key_exists("class", $props)) {
$classes = explode(" ",trim($props['class']));
foreach ($classes as $class) {
$rule = $style->select(".".$class);
$props = array_merge($rule, $props);
}
}
foreach ($props as $prop=>$value) {
$elem->setProp($prop, $value);
}
}
private function camelToDash($name)
{
return strtolower(preg_replace("/([A-Z]{1})/", "-\$1", $name));
}
}

75
src/Renderer.php Normal file
View File

@ -0,0 +1,75 @@
<?php
namespace NoccyLabs\GdLabel;
use NoccyLabs\GdLabel\Loader\XmlLoader;
class Renderer
{
public function renderFile($inputFile, array $params=[], $output=null)
{
$loader = new XmlLoader();
$label = $loader->loadFile($inputFile);
$rendered = $this->render($label, $params);
if ($output) {
$ext = pathinfo($output, \PATHINFO_EXTENSION);
switch (strtolower($ext)) {
case 'png': imagepng($rendered, $output); break;
case 'jpg': imagejpeg($rendered, $output); break;
default:
throw new \InvalidArgumentException("Can't write {$ext} yet");
}
}
return $rendered;
}
public function render(Label $label, array $params=[])
{
$width = $label->getProp("width");
$height = $label->getProp("height");
$gd = imagecreatetruecolor($width, $height);
if (is_callable("imageantialias")) {
\imageantialias($gd, true);
}
$this->renderElement($gd, $label, 0, 0, $width, $height, $params);
return $gd;
}
protected function renderElement($gd, Element $element, int $x, int $y, int $width, int $height, array $params)
{
$xOffs = $x + $element->getProp("left", 0);
$yOffs = $y + $element->getProp("top", 0);
if (($widthAttr = $element->getProp("width",0))) {
$elemWidth = $widthAttr;
} else {
$rightAttr = $element->getProp("right",0);
$elemWidth = $width - $rightAttr - $element->getProp("left",0);;
}
if (($heightAttr = $element->getProp("height",0))) {
$elemHeight = $heightAttr;
} else {
$bottomAttr = $element->getProp("bottom",0);
$elemHeight = $height - $bottomAttr - $element->getProp("top",0);
}
// printf("render[%s] at {%dx%d}→{%dx%d}\n", get_class($element), $xOffs, $yOffs, $xOffs+$elemWidth, $yOffs+$elemHeight);
$element->drawElement($gd, $xOffs, $yOffs, $elemWidth, $elemHeight, $params);
foreach ($element->getChildren() as $child) {
$this->renderElement($gd, $child, $xOffs, $yOffs, $elemWidth, $elemHeight, $params);
}
}
}