commit a410ce8b19655844feda2455a11c8f1f80f9e872 Author: Christopher Vagnetoft Date: Thu Oct 26 00:42:37 2017 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ff72e2d --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/composer.lock +/vendor diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..5661fee --- /dev/null +++ b/composer.json @@ -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/" + } + ] +} diff --git a/src/Element.php b/src/Element.php new file mode 100644 index 0000000..b9a3249 --- /dev/null +++ b/src/Element.php @@ -0,0 +1,94 @@ +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)); + } + +} \ No newline at end of file diff --git a/src/Elements/BarcodeElement.php b/src/Elements/BarcodeElement.php new file mode 100644 index 0000000..339f31a --- /dev/null +++ b/src/Elements/BarcodeElement.php @@ -0,0 +1,98 @@ +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; + } + +} \ No newline at end of file diff --git a/src/Elements/FrameElement.php b/src/Elements/FrameElement.php new file mode 100644 index 0000000..4f2f410 --- /dev/null +++ b/src/Elements/FrameElement.php @@ -0,0 +1,39 @@ +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); + + } + +} \ No newline at end of file diff --git a/src/Elements/ImageElement.php b/src/Elements/ImageElement.php new file mode 100644 index 0000000..7dc6398 --- /dev/null +++ b/src/Elements/ImageElement.php @@ -0,0 +1,31 @@ +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); + + } + + +} \ No newline at end of file diff --git a/src/Elements/StringElement.php b/src/Elements/StringElement.php new file mode 100644 index 0000000..ff967d0 --- /dev/null +++ b/src/Elements/StringElement.php @@ -0,0 +1,46 @@ +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); + + } + + +} \ No newline at end of file diff --git a/src/Elements/TextElement.php b/src/Elements/TextElement.php new file mode 100644 index 0000000..7964786 --- /dev/null +++ b/src/Elements/TextElement.php @@ -0,0 +1,72 @@ +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); + + } + + +} \ No newline at end of file diff --git a/src/Label.php b/src/Label.php new file mode 100644 index 0000000..989edf8 --- /dev/null +++ b/src/Label.php @@ -0,0 +1,17 @@ +getPropRgb("background-color")) { + \imagefilledrectangle($gd, $x, $y, $x + $width, $y + $height, $bg); + } + + + } + +} \ No newline at end of file diff --git a/src/Loader/XmlLoader.php b/src/Loader/XmlLoader.php new file mode 100644 index 0000000..53fdeaa --- /dev/null +++ b/src/Loader/XmlLoader.php @@ -0,0 +1,98 @@ +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)); + } + +} \ No newline at end of file diff --git a/src/Renderer.php b/src/Renderer.php new file mode 100644 index 0000000..1b38a57 --- /dev/null +++ b/src/Renderer.php @@ -0,0 +1,75 @@ +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); + } + } +} \ No newline at end of file