From 39d711cb46d3c93cd19b2490c321b09c2d2741d0 Mon Sep 17 00:00:00 2001 From: vatsake Date: Wed, 1 May 2024 17:08:14 +0300 Subject: [PATCH 01/10] cups --- composer.json | 154 ++++++++-------- config/printing.php | 1 + src/Api/Cups/AttributeGroup.php | 79 +++++++++ src/Api/Cups/AttributeGroupTag.php | 30 ++++ src/Api/Cups/Attributes/JobGroup.php | 13 ++ src/Api/Cups/Attributes/OperationGroup.php | 13 ++ src/Api/Cups/Attributes/PrinterGroup.php | 13 ++ src/Api/Cups/Attributes/UnsupportedGroup.php | 13 ++ src/Api/Cups/Cups.php | 43 +++++ src/Api/Cups/Exceptions/ClientError.php | 15 ++ src/Api/Cups/Exceptions/RangeOverlap.php | 15 ++ src/Api/Cups/Exceptions/ServerError.php | 15 ++ src/Api/Cups/Exceptions/UnknownEnum.php | 15 ++ src/Api/Cups/Exceptions/UnknownType.php | 15 ++ src/Api/Cups/Operation.php | 95 ++++++++++ src/Api/Cups/Request.php | 119 +++++++++++++ src/Api/Cups/Response.php | 166 ++++++++++++++++++ src/Api/Cups/Type.php | 33 ++++ src/Api/Cups/TypeTag.php | 73 ++++++++ src/Api/Cups/Types/Charset.php | 13 ++ src/Api/Cups/Types/DateTime.php | 64 +++++++ src/Api/Cups/Types/MimeMedia.php | 13 ++ src/Api/Cups/Types/NameWithoutLanguage.php | 13 ++ src/Api/Cups/Types/NaturalLanguage.php | 13 ++ src/Api/Cups/Types/Primitive/Boolean.php | 23 +++ src/Api/Cups/Types/Primitive/Enum.php | 23 +++ src/Api/Cups/Types/Primitive/Integer.php | 23 +++ src/Api/Cups/Types/Primitive/Keyword.php | 23 +++ src/Api/Cups/Types/Primitive/NoValue.php | 23 +++ src/Api/Cups/Types/Primitive/OctetString.php | 23 +++ src/Api/Cups/Types/Primitive/Text.php | 23 +++ src/Api/Cups/Types/Primitive/Unknown.php | 23 +++ src/Api/Cups/Types/RangeOfInteger.php | 66 +++++++ src/Api/Cups/Types/Resolution.php | 35 ++++ src/Api/Cups/Types/TextWithoutLanguage.php | 13 ++ src/Api/Cups/Types/Uri.php | 13 ++ src/Api/Cups/Version.php | 15 ++ src/Drivers/Cups/ContentType.php | 41 ++++- src/Drivers/Cups/Cups.php | 156 ++++++++-------- src/Drivers/Cups/Entity/PrintJob.php | 57 ++++-- src/Drivers/Cups/Entity/Printer.php | 105 +++++------ src/Drivers/Cups/Enum/JobState.php | 16 ++ src/Drivers/Cups/Enum/PrinterState.php | 12 ++ src/Drivers/Cups/Orientation.php | 13 ++ src/Drivers/Cups/PrintTask.php | 162 ++++++++--------- src/Drivers/Cups/Sides.php | 13 ++ src/Drivers/Cups/Support/Client.php | 119 ------------- src/Factory.php | 8 +- src/PrintingServiceProvider.php | 9 + tests/Feature/Drivers/Cups/Entity/JobTest.php | 19 +- .../Drivers/Cups/Entity/PrinterTest.php | 40 +---- tests/Pest.php | 32 ++-- 52 files changed, 1647 insertions(+), 517 deletions(-) create mode 100644 src/Api/Cups/AttributeGroup.php create mode 100644 src/Api/Cups/AttributeGroupTag.php create mode 100644 src/Api/Cups/Attributes/JobGroup.php create mode 100644 src/Api/Cups/Attributes/OperationGroup.php create mode 100644 src/Api/Cups/Attributes/PrinterGroup.php create mode 100644 src/Api/Cups/Attributes/UnsupportedGroup.php create mode 100644 src/Api/Cups/Cups.php create mode 100644 src/Api/Cups/Exceptions/ClientError.php create mode 100644 src/Api/Cups/Exceptions/RangeOverlap.php create mode 100644 src/Api/Cups/Exceptions/ServerError.php create mode 100644 src/Api/Cups/Exceptions/UnknownEnum.php create mode 100644 src/Api/Cups/Exceptions/UnknownType.php create mode 100644 src/Api/Cups/Operation.php create mode 100644 src/Api/Cups/Request.php create mode 100644 src/Api/Cups/Response.php create mode 100644 src/Api/Cups/Type.php create mode 100644 src/Api/Cups/TypeTag.php create mode 100644 src/Api/Cups/Types/Charset.php create mode 100644 src/Api/Cups/Types/DateTime.php create mode 100644 src/Api/Cups/Types/MimeMedia.php create mode 100644 src/Api/Cups/Types/NameWithoutLanguage.php create mode 100644 src/Api/Cups/Types/NaturalLanguage.php create mode 100644 src/Api/Cups/Types/Primitive/Boolean.php create mode 100644 src/Api/Cups/Types/Primitive/Enum.php create mode 100644 src/Api/Cups/Types/Primitive/Integer.php create mode 100644 src/Api/Cups/Types/Primitive/Keyword.php create mode 100644 src/Api/Cups/Types/Primitive/NoValue.php create mode 100644 src/Api/Cups/Types/Primitive/OctetString.php create mode 100644 src/Api/Cups/Types/Primitive/Text.php create mode 100644 src/Api/Cups/Types/Primitive/Unknown.php create mode 100644 src/Api/Cups/Types/RangeOfInteger.php create mode 100644 src/Api/Cups/Types/Resolution.php create mode 100644 src/Api/Cups/Types/TextWithoutLanguage.php create mode 100644 src/Api/Cups/Types/Uri.php create mode 100644 src/Api/Cups/Version.php create mode 100644 src/Drivers/Cups/Enum/JobState.php create mode 100644 src/Drivers/Cups/Enum/PrinterState.php create mode 100644 src/Drivers/Cups/Orientation.php create mode 100644 src/Drivers/Cups/Sides.php delete mode 100644 src/Drivers/Cups/Support/Client.php diff --git a/composer.json b/composer.json index 062e968..bbe315b 100644 --- a/composer.json +++ b/composer.json @@ -1,82 +1,78 @@ { - "name": "rawilk/laravel-printing", - "description": "Direct printing for Laravel apps", - "keywords": [ - "rawilk", - "laravel-printing", - "PrintNode", - "CUPS", - "ipp", - "Receipt printing", - "Direct printing", - "Raw printing" + "name": "rawilk/laravel-printing", + "description": "Direct printing for Laravel apps", + "keywords": [ + "rawilk", + "laravel-printing", + "PrintNode", + "CUPS", + "ipp", + "Receipt printing", + "Direct printing", + "Raw printing" + ], + "homepage": "https://github.com/rawilk/laravel-printing", + "license": "MIT", + "authors": [ + { + "name": "Randall Wilk", + "email": "randall@randallwilk.dev", + "homepage": "https://randallwilk.dev", + "role": "Developer" + } + ], + "require": { + "php": "^8.0|^8.1|^8.2|^8.3", + "guzzlehttp/guzzle": "^7.5", + "illuminate/support": "^8.0|^9.0|^10.0|^11.0", + "mike42/escpos-php": "^4.0", + "spatie/laravel-package-tools": "^1.2|^1.13" + }, + "require-dev": { + "laravel/pint": "^1.5", + "mockery/mockery": ">=1.4", + "orchestra/testbench": "^6.0|^7.0|^8.0|^9.0", + "pestphp/pest": "^1.20|^2.34", + "pestphp/pest-plugin-laravel": "^1.0|^2.2", + "php-http/socket-client": "^2.1", + "php-http/message-factory": "^1.1", + "psr/http-message": "1.*", + "psr/http-client": "^1.0", + "spatie/laravel-ray": "^1.0|^1.29" + }, + "autoload": { + "psr-4": { + "Rawilk\\Printing\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "Rawilk\\Printing\\Tests\\": "tests" + } + }, + "scripts": { + "post-autoload-dump": [ + "@php ./vendor/bin/testbench package:discover --ansi" ], - "homepage": "https://github.com/rawilk/laravel-printing", - "license": "MIT", - "authors": [ - { - "name": "Randall Wilk", - "email": "randall@randallwilk.dev", - "homepage": "https://randallwilk.dev", - "role": "Developer" - } - ], - "require": { - "php": "^8.0|^8.1|^8.2|^8.3", - "guzzlehttp/guzzle": "^7.5", - "illuminate/support": "^8.0|^9.0|^10.0|^11.0", - "mike42/escpos-php": "^4.0", - "spatie/laravel-package-tools": "^1.2|^1.13" - }, - "require-dev": { - "laravel/pint": "^1.5", - "mockery/mockery": ">=1.4", - "orchestra/testbench": "^6.0|^7.0|^8.0|^9.0", - "pestphp/pest": "^1.20|^2.34", - "pestphp/pest-plugin-laravel": "^1.0|^2.2", - "php-http/socket-client": "^2.1", - "php-http/message-factory": "^1.1", - "psr/http-message": "1.*", - "psr/http-client": "^1.0", - "smalot/cups-ipp": "^0.5.0", - "spatie/laravel-ray": "^1.0|^1.29" - }, - "suggest": { - "smalot/cups-ipp": "Required when using the CUPS driver" - }, - "autoload": { - "psr-4": { - "Rawilk\\Printing\\": "src" - } - }, - "autoload-dev": { - "psr-4": { - "Rawilk\\Printing\\Tests\\": "tests" - } - }, - "scripts": { - "post-autoload-dump": [ - "@php ./vendor/bin/testbench package:discover --ansi" - ], - "test": "vendor/bin/pest -p", - "format": "vendor/bin/pint --dirty" - }, - "config": { - "sort-packages": true, - "allow-plugins": { - "pestphp/pest-plugin": true - } - }, - "extra": { - "laravel": { - "providers": [ - "Rawilk\\Printing\\PrintingServiceProvider" - ], - "aliases": { - "Printing": "Rawilk\\Printing\\Facades\\Printing" - } - } - }, - "minimum-stability": "dev", - "prefer-stable": true + "test": "vendor/bin/pest -p", + "format": "vendor/bin/pint --dirty" + }, + "config": { + "sort-packages": true, + "allow-plugins": { + "pestphp/pest-plugin": true + } + }, + "extra": { + "laravel": { + "providers": [ + "Rawilk\\Printing\\PrintingServiceProvider" + ], + "aliases": { + "Printing": "Rawilk\\Printing\\Facades\\Printing" + } + } + }, + "minimum-stability": "dev", + "prefer-stable": true } diff --git a/config/printing.php b/config/printing.php index 49a6fcb..cd3ca6e 100644 --- a/config/printing.php +++ b/config/printing.php @@ -30,6 +30,7 @@ 'username' => env('CUPS_SERVER_USERNAME'), 'password' => env('CUPS_SERVER_PASSWORD'), 'port' => env('CUPS_SERVER_PORT', 631), + 'secure' => env('CUPS_SERVER_SECURE', false), ], /* diff --git a/src/Api/Cups/AttributeGroup.php b/src/Api/Cups/AttributeGroup.php new file mode 100644 index 0000000..33944c0 --- /dev/null +++ b/src/Api/Cups/AttributeGroup.php @@ -0,0 +1,79 @@ + + */ + protected array $attributes = []; + + public function __set($name, $value) + { + $this->attributes[$name] = $value; + } + + public function __construct(array $attributes = []) + { + $this->attributes = $attributes; + } + + /** + * @var array + */ + public function getAttributes() + { + return $this->attributes; + } + + public function encode(): string + { + $binary = pack('c', $this->tag); + foreach ($this->attributes as $name => $value) { + if (gettype($value->value) === 'array') { + $binary .= $this->handleArrayEncode($name, $value); + continue; + } + $nameLen = strlen($name); + + $binary .= pack('c', $value->getTag()); + $binary .= pack('n', $nameLen); // Attribute key length + $binary .= pack('a' . $nameLen, $name); // Attribute key + $binary .= $value->encode(); // Attribute value (with length) + } + return $binary; + } + + /** + * If attribute is an array, the attribute name after the first element is empty + */ + private function handleArrayEncode(string $name, \Rawilk\Printing\Api\Cups\Type $value): string + { + $str = ''; + for ($i = 0; $i < sizeof($value->value); $i++) { + $_name = $name; + if ($i !== 0) { + $_name = ''; + } + $nameLen = strlen($_name); + + $str .= pack('c', $value->getTag()); // Value tag + $str .= pack('n', $nameLen); // Attribute key length + $str .= pack('a' . $nameLen, $_name); // Attribute key + + $class = $value::class; + $str .= (new $class($value->value[$i]))->encode(); + } + return $str; + } +} diff --git a/src/Api/Cups/AttributeGroupTag.php b/src/Api/Cups/AttributeGroupTag.php new file mode 100644 index 0000000..c028915 --- /dev/null +++ b/src/Api/Cups/AttributeGroupTag.php @@ -0,0 +1,30 @@ +value => JobGroup::class, + AttributeGroupTag::OPERATION_ATTRIBUTES->value => OperationGroup::class, + AttributeGroupTag::PRINTER_ATTRIBUTES->value => PrinterGroup::class, + AttributeGroupTag::UNSUPPORTED_ATTRIBUTES->value => UnsupportedGroup::class, + }; + } +} diff --git a/src/Api/Cups/Attributes/JobGroup.php b/src/Api/Cups/Attributes/JobGroup.php new file mode 100644 index 0000000..ea1d536 --- /dev/null +++ b/src/Api/Cups/Attributes/JobGroup.php @@ -0,0 +1,13 @@ +value; +} diff --git a/src/Api/Cups/Attributes/OperationGroup.php b/src/Api/Cups/Attributes/OperationGroup.php new file mode 100644 index 0000000..d1977b7 --- /dev/null +++ b/src/Api/Cups/Attributes/OperationGroup.php @@ -0,0 +1,13 @@ +value; +} diff --git a/src/Api/Cups/Attributes/PrinterGroup.php b/src/Api/Cups/Attributes/PrinterGroup.php new file mode 100644 index 0000000..8891b94 --- /dev/null +++ b/src/Api/Cups/Attributes/PrinterGroup.php @@ -0,0 +1,13 @@ +value; +} diff --git a/src/Api/Cups/Attributes/UnsupportedGroup.php b/src/Api/Cups/Attributes/UnsupportedGroup.php new file mode 100644 index 0000000..abef28c --- /dev/null +++ b/src/Api/Cups/Attributes/UnsupportedGroup.php @@ -0,0 +1,13 @@ +value; +} diff --git a/src/Api/Cups/Cups.php b/src/Api/Cups/Cups.php new file mode 100644 index 0000000..af9b19a --- /dev/null +++ b/src/Api/Cups/Cups.php @@ -0,0 +1,43 @@ +encode())->withHeaders( + [ + "Content-Type" => "application/ipp" + ] + )->post($this->getScheme() . '://' . $this->ip . ':' . $this->port . '/admin') + ->throwIfClientError(); + $response = new Response($http->body()); + + return $response; + } + + private function getScheme() + { + return $this->secure ? 'https' : 'http'; + } +} diff --git a/src/Api/Cups/Exceptions/ClientError.php b/src/Api/Cups/Exceptions/ClientError.php new file mode 100644 index 0000000..87b44ec --- /dev/null +++ b/src/Api/Cups/Exceptions/ClientError.php @@ -0,0 +1,15 @@ +addOperationAttributes( + [ + 'attributes-charset' => new Charset('utf-8'), + 'attributes-natural-language' => new NaturalLanguage('en'), + ] + ); + } + + public function setVersion(Version $version) + { + $this->version = $version; + return $this; + } + + /** + * @see \Rawilk\Printing\Api\Cups\Operation Operations supported + */ + public function setOperation($operation) + { + $this->operation = $operation; + return $this; + } + + /** + * Set file contents to print + */ + public function setContent(string $content) + { + $this->content = $content; + } + + /** + * You may optionally specify the request ID, default is 1 + */ + public function setRequestId(int $requestId) + { + $this->requestId = $requestId; + return $this; + } + + /** + * @param array $attributes + */ + public function addOperationAttributes(array $attributes) + { + $this->setAttributes(\Rawilk\Printing\Api\Cups\Attributes\OperationGroup::class, $attributes); + return $this; + } + + /** + * @param array $attributes + */ + public function addJobAttributes(array $attributes) + { + $this->setAttributes(\Rawilk\Printing\Api\Cups\Attributes\JobGroup::class, $attributes); + return $this; + } + + private function setAttributes(string $className, array $attributes) + { + $index = $this->getGroupIndex($className); + foreach ($attributes as $name => $value) { + $this->attributeGroups[$index]->$name = $value; + } + } + + private function getGroupIndex(string $className): int + { + for ($i = 0; $i < sizeof($this->attributeGroups); $i++) { + if ($this->attributeGroups[$i] instanceof $className) { + return $i; + } + } + $this->attributeGroups[] = new $className(); + return sizeof($this->attributeGroups) - 1; + } + + public function encode() + { + + $binary = $this->version->encode(); + $binary .= pack('n', $this->operation); + $binary .= pack('N', $this->requestId); + + foreach ($this->attributeGroups as $group) { + $binary .= $group->encode(); + } + $binary .= pack('c', AttributeGroupTag::END_OF_ATTRIBUTES->value); + + if ($this->content) { + $binary .= $this->content; + } + + return $binary; + } +} diff --git a/src/Api/Cups/Response.php b/src/Api/Cups/Response.php new file mode 100644 index 0000000..99ac4ef --- /dev/null +++ b/src/Api/Cups/Response.php @@ -0,0 +1,166 @@ +decode($binaryData); + } + + public function getVersion() + { + return $this->version; + } + + public function getRequestId() + { + return $this->requestId; + } + + private function decode(string $binary) + { + $data = unpack("cmajorVer/cminorVer/ncode/NrequestId/ctag", $binary); + + $this->statusCode = $data['code']; + $this->version = Version::tryFrom($data['majorVer'] . '.' . $data['minorVer']); + $this->requestId = $data['requestId']; + + $nextTag = $data['tag']; + $offset = 9; + + $this->attributeGroups = []; + while (AttributeGroupTag::tryFrom($nextTag) && $nextTag !== AttributeGroupTag::END_OF_ATTRIBUTES->value) { + $currentTag = $nextTag; + $attributes = $this->extractAttributes($binary, $offset, $nextTag); + $className = AttributeGroupTag::getGroupClassByTag($currentTag); + $this->attributeGroups[] = new $className($attributes); + } + + $this->checkSuccessfulResponse(); + } + + private function extractAttributes(string $binary, int &$offset, mixed &$nextTag) + { + $attributes = []; + $nextTag = -1; + while (!AttributeGroupTag::tryFrom($nextTag)) { + $typeTag = (unpack('ctypeTag', $binary, $offset))['typeTag']; + $type = TypeTag::tryFrom($typeTag); + $offset++; + + $nameLen = (unpack('n', $binary, $offset))[1]; + $offset += 2; + + $attrName = unpack('a' . $nameLen, $binary, $offset)[1]; + $offset += $nameLen; + + if (!$type) { + throw new UnknownType("Unknown type tag \"$typeTag\" for attribute \"$attrName\"."); + } + + $valueLen = (unpack('n', $binary, $offset))[1]; + $offset += 2; + + $typeClass = $type->getClass(); + $attribute = $typeClass::fromBinary(substr($binary, $offset, $valueLen), $valueLen); + $offset += $valueLen; + + // Array of values + if ($attrName === '') { + $lastAttr = $attributes[array_key_last($attributes)]; + + if ($typeTag !== TypeTag::RANGEOFINTEGER->value && gettype($lastAttr->value) !== 'array') { + $lastAttr->value = [$lastAttr->value]; + } + + if ($typeTag == TypeTag::RANGEOFINTEGER->value) { + $lastAttr->value[] = $attribute->value[0]; + } else { + $lastAttr->value[] = $attribute->value; + } + } else { + $attributes[$attrName] = $attribute; + } + + $nextTag = (unpack("ctag", $binary, $offset))['tag']; + } + $offset++; + + return $attributes; + } + + private function checkSuccessfulResponse() + { + if ($this->statusCode >= 0x0400 && $this->statusCode <= 0x04FF) { + throw new \Rawilk\Printing\Api\Cups\Exceptions\ClientError($this->getStatusMessage()); + } elseif ($this->statusCode >= 0x0500 && $this->statusCode <= 0x05FF) { + throw new \Rawilk\Printing\Api\Cups\Exceptions\ClientError($this->getStatusMessage()); + } + } + + private function getStatusMessage(): string + { + $group = $this->attributeGroups[$this->getGroupIndex(\Rawilk\Printing\Api\Cups\Attributes\OperationGroup::class)]; + $attributes = $group->getAttributes(); + if (array_key_exists('status-message', $attributes)) { + return $attributes['status-message']->value; + } + return ''; + } + + private function getGroupIndex(string $className): int + { + for ($i = 0; $i < sizeof($this->attributeGroups); $i++) { + if ($this->attributeGroups[$i] instanceof $className) { + return $i; + } + } + $this->attributeGroups[] = new $className(); + return sizeof($this->attributeGroups) - 1; + } + + /** + * @return \Illuminate\Support\Collection + */ + public function getPrinters() + { + $printers = collect(); + foreach ($this->attributeGroups as $group) { + if ($group instanceof \Rawilk\Printing\Api\Cups\Attributes\PrinterGroup) { + $printers->push(new Printer($group->getAttributes())); + } + } + return $printers; + } + + /** + * @return \Illuminate\Support\Collection + */ + public function getJobs() + { + $jobs = collect(); + foreach ($this->attributeGroups as $group) { + if ($group instanceof \Rawilk\Printing\Api\Cups\Attributes\JobGroup) { + $jobs->push(new PrintJob($group->getAttributes())); + } + } + return $jobs; + } +} diff --git a/src/Api/Cups/Type.php b/src/Api/Cups/Type.php new file mode 100644 index 0000000..a15af23 --- /dev/null +++ b/src/Api/Cups/Type.php @@ -0,0 +1,33 @@ +tag; + } + + public function jsonSerialize(): mixed + { + return $this->value; + } +} diff --git a/src/Api/Cups/TypeTag.php b/src/Api/Cups/TypeTag.php new file mode 100644 index 0000000..ad16ffa --- /dev/null +++ b/src/Api/Cups/TypeTag.php @@ -0,0 +1,73 @@ +value) { + self::CHARSET->value => Charset::class, + self::NATURALLANGUAGE->value => NaturalLanguage::class, + self::OCTETSTRING->value => OctetString::class, + self::INTEGER->value => Integer::class, + self::DATETIME->value => DateTime::class, + self::NOVALUE->value => NoValue::class, + self::NAMEWITHOUTLANGUAGE->value => NameWithoutLanguage::class, + self::URI->value => Uri::class, + self::BOOLEAN->value => Boolean::class, + self::ENUM->value => Enum::class, + self::TEXTWITHOUTLANGUAGE->value => TextWithoutLanguage::class, + self::KEYWORD->value => Keyword::class, + self::UNKNOWN->value => Unknown::class, + self::MIMEMEDIATYPE->value => MimeMedia::class, + self::RESOLUTION->value => Resolution::class, + self::RANGEOFINTEGER->value => RangeOfInteger::class, + }; + } +} diff --git a/src/Api/Cups/Types/Charset.php b/src/Api/Cups/Types/Charset.php new file mode 100644 index 0000000..7cab7db --- /dev/null +++ b/src/Api/Cups/Types/Charset.php @@ -0,0 +1,13 @@ +value; +} diff --git a/src/Api/Cups/Types/DateTime.php b/src/Api/Cups/Types/DateTime.php new file mode 100644 index 0000000..a327ae6 --- /dev/null +++ b/src/Api/Cups/Types/DateTime.php @@ -0,0 +1,64 @@ +value; + + /** + * @param Carbon $value + */ + public function __construct(public mixed $value) + { + } + + public function encode(): string + { + preg_match('/([+-])(\d{2}):(\d{2})/', $this->value->getOffsetString(), $matches); + return pack('n', 11) . pack('n', $this->value->format('Y')) + . pack('c', $this->value->format('m')) + . pack('c', $this->value->format('d')) + . pack('c', $this->value->format('H')) + . pack('c', $this->value->format('i')) + . pack('c', $this->value->format('s')) + . pack('c', 0) + . pack('a', $matches[1]) + . pack('c', self::unpad($matches[2])) + . pack('c', self::unpad($matches[3])); + } + + public static function fromBinary(string $binary, ?int $length = null): self + { + $data = unpack('nY/cm/cd/cH/ci/cs/cfff/aUTCSym/cUTCm/cUTCs', $binary); + return new static( + Carbon::createFromFormat( + 'YmdHisO', + $data['Y'] + . str_pad((string) $data['m'], 2, '0', STR_PAD_LEFT) + . str_pad((string) $data['d'], 2, '0', STR_PAD_LEFT) + . str_pad((string) $data['H'], 2, '0', STR_PAD_LEFT) + . str_pad((string) $data['i'], 2, '0', STR_PAD_LEFT) + . str_pad((string) $data['s'], 2, '0', STR_PAD_LEFT) + . $data['UTCSym'] + . str_pad((string)$data['UTCm'], 2, '0', STR_PAD_LEFT) + . str_pad((string)$data['UTCs'], 2, '0', STR_PAD_LEFT) + ) + ); + } + + private static function unpad(string $str) + { + $unpaddedStr = ltrim($str, '0'); + if ($unpaddedStr === '') { + $unpaddedStr = '0'; // Ensure "00" becomes "0" + } + return $unpaddedStr; + } +} diff --git a/src/Api/Cups/Types/MimeMedia.php b/src/Api/Cups/Types/MimeMedia.php new file mode 100644 index 0000000..6497896 --- /dev/null +++ b/src/Api/Cups/Types/MimeMedia.php @@ -0,0 +1,13 @@ +value; +} diff --git a/src/Api/Cups/Types/NameWithoutLanguage.php b/src/Api/Cups/Types/NameWithoutLanguage.php new file mode 100644 index 0000000..09e6365 --- /dev/null +++ b/src/Api/Cups/Types/NameWithoutLanguage.php @@ -0,0 +1,13 @@ +value; +} diff --git a/src/Api/Cups/Types/NaturalLanguage.php b/src/Api/Cups/Types/NaturalLanguage.php new file mode 100644 index 0000000..fc05eab --- /dev/null +++ b/src/Api/Cups/Types/NaturalLanguage.php @@ -0,0 +1,13 @@ +value; +} diff --git a/src/Api/Cups/Types/Primitive/Boolean.php b/src/Api/Cups/Types/Primitive/Boolean.php new file mode 100644 index 0000000..6b3ea41 --- /dev/null +++ b/src/Api/Cups/Types/Primitive/Boolean.php @@ -0,0 +1,23 @@ +value; + + public function encode(): string + { + return pack('n', 1) . pack('c', intval($this->value)); + } + + public static function fromBinary(string $binary, ?int $length = null): self + { + return new static((bool) unpack('c', $binary)[1]); + } +} diff --git a/src/Api/Cups/Types/Primitive/Enum.php b/src/Api/Cups/Types/Primitive/Enum.php new file mode 100644 index 0000000..75765f9 --- /dev/null +++ b/src/Api/Cups/Types/Primitive/Enum.php @@ -0,0 +1,23 @@ +value; + + public function encode(): string + { + return pack('n', 4) . pack('N', $this->value); + } + + public static function fromBinary(string $binary, ?int $length = null): self + { + return new static(unpack('N', $binary)[1]); + } +} diff --git a/src/Api/Cups/Types/Primitive/Integer.php b/src/Api/Cups/Types/Primitive/Integer.php new file mode 100644 index 0000000..5d72349 --- /dev/null +++ b/src/Api/Cups/Types/Primitive/Integer.php @@ -0,0 +1,23 @@ +value; + + public function encode(): string + { + return pack('n', strlen($this->value)) . pack('N', $this->value); + } + + public static function fromBinary(string $binary, ?int $length = null): self + { + return new static(unpack('N', $binary)[1]); + } +} diff --git a/src/Api/Cups/Types/Primitive/Keyword.php b/src/Api/Cups/Types/Primitive/Keyword.php new file mode 100644 index 0000000..6dd7bb2 --- /dev/null +++ b/src/Api/Cups/Types/Primitive/Keyword.php @@ -0,0 +1,23 @@ +value; + + public function encode(): string + { + return pack('n', strlen($this->value)) . pack('a' . strlen($this->value), $this->value); + } + + public static function fromBinary(string $binary, ?int $length = null): self + { + return new static(unpack('a' . $length, $binary)[1]); + } +} diff --git a/src/Api/Cups/Types/Primitive/NoValue.php b/src/Api/Cups/Types/Primitive/NoValue.php new file mode 100644 index 0000000..001b7e0 --- /dev/null +++ b/src/Api/Cups/Types/Primitive/NoValue.php @@ -0,0 +1,23 @@ +value; + + public function encode(): string + { + return pack('n', 0) . ''; + } + + public static function fromBinary(string $binary, ?int $length = null): self + { + return new static(null); + } +} diff --git a/src/Api/Cups/Types/Primitive/OctetString.php b/src/Api/Cups/Types/Primitive/OctetString.php new file mode 100644 index 0000000..37c344e --- /dev/null +++ b/src/Api/Cups/Types/Primitive/OctetString.php @@ -0,0 +1,23 @@ +value; + + public function encode(): string + { + return pack('a', strlen($this->value)) . pack('a' . strlen($this->value), $this->value); + } + + public static function fromBinary(string $binary, ?int $length = null): self + { + return new static(unpack('a' . $length, $binary)[1]); + } +} diff --git a/src/Api/Cups/Types/Primitive/Text.php b/src/Api/Cups/Types/Primitive/Text.php new file mode 100644 index 0000000..f9e84bd --- /dev/null +++ b/src/Api/Cups/Types/Primitive/Text.php @@ -0,0 +1,23 @@ +value; + + public function encode(): string + { + return pack('n', strlen($this->value)) . pack('a' . strlen($this->value), $this->value); + } + + public static function fromBinary(string $binary, ?int $length = null): self + { + return new static(unpack('a' . $length, $binary)[1]); + } +} diff --git a/src/Api/Cups/Types/Primitive/Unknown.php b/src/Api/Cups/Types/Primitive/Unknown.php new file mode 100644 index 0000000..9ac5a77 --- /dev/null +++ b/src/Api/Cups/Types/Primitive/Unknown.php @@ -0,0 +1,23 @@ +value; + + public function encode(): string + { + return pack('n', 0) . ''; + } + + public static function fromBinary(string $binary, ?int $length = null): self + { + return new static(null); + } +} diff --git a/src/Api/Cups/Types/RangeOfInteger.php b/src/Api/Cups/Types/RangeOfInteger.php new file mode 100644 index 0000000..7b62886 --- /dev/null +++ b/src/Api/Cups/Types/RangeOfInteger.php @@ -0,0 +1,66 @@ +value; + + /** + * @param array|int[] $value + */ + public function __construct(public mixed $value) + { + parent::__construct($value); + $this->checkOverlaps(); + } + + public function encode(): string + { + return pack('n', 8) . pack('N', $this->value[0]) . pack('N', $this->value[1]); + } + + public static function fromBinary(string $binary, ?int $length = null): self + { + $value = unpack('Nl/Nu', $binary); + return new static([[$value['l'], $value['u']]]); + } + + public function addRange($lower, $upper) + { + $this->value[] = [$lower, $upper]; + $this->checkOverlaps(); + } + + private function sortValues() + { + usort( + $this->value, + function ($a, $b) { + return $a[0] - $b[0]; + } + ); + } + + private function checkOverlaps() + { + if (gettype($this->value[0]) !== 'array') { + return; + } + $this->sortValues(); + $ranges = $this->value; + + $count = count($ranges); + for ($i = 0; $i < $count - 1; $i++) { + if ($ranges[$i][1] >= $ranges[$i + 1][0]) { + throw new \Rawilk\Printing\Api\Cups\Exceptions\RangeOverlap('Range overlap is not allowed!'); + } + } + return true; // No overlaps found + } +} diff --git a/src/Api/Cups/Types/Resolution.php b/src/Api/Cups/Types/Resolution.php new file mode 100644 index 0000000..cd24d1d --- /dev/null +++ b/src/Api/Cups/Types/Resolution.php @@ -0,0 +1,35 @@ +value; + + private static $unitMap = [ + 3 => 'dpi', + 4 => 'dpc', + ]; + + public function encode(): string + { + preg_match('/(\d+)x(\d+)(.*)/', $this->value, $matches); + $reverseMap = array_flip(static::$unitMap); + + return pack('n', 9) . pack('N', $matches[1]) + . pack('N', $matches[2]) + . pack('c', $reverseMap[$matches[3]]); + } + + + public static function decode(string $binary, ?int $length = null): mixed + { + $value = unpack('Np/Np2/cu', $binary); + return $value['p'] . 'x' . $value['p2'] . static::$unitMap[$value['u']]; + } +} diff --git a/src/Api/Cups/Types/TextWithoutLanguage.php b/src/Api/Cups/Types/TextWithoutLanguage.php new file mode 100644 index 0000000..60ea707 --- /dev/null +++ b/src/Api/Cups/Types/TextWithoutLanguage.php @@ -0,0 +1,13 @@ +value; +} diff --git a/src/Api/Cups/Types/Uri.php b/src/Api/Cups/Types/Uri.php new file mode 100644 index 0000000..7399509 --- /dev/null +++ b/src/Api/Cups/Types/Uri.php @@ -0,0 +1,13 @@ +value; +} diff --git a/src/Api/Cups/Version.php b/src/Api/Cups/Version.php new file mode 100644 index 0000000..7b69489 --- /dev/null +++ b/src/Api/Cups/Version.php @@ -0,0 +1,15 @@ +value); + return pack('c', $version[0]) . pack('c', $version[1]); + } +} diff --git a/src/Drivers/Cups/ContentType.php b/src/Drivers/Cups/ContentType.php index 98c4f1c..9120ba5 100644 --- a/src/Drivers/Cups/ContentType.php +++ b/src/Drivers/Cups/ContentType.php @@ -6,12 +6,37 @@ class ContentType { - /** @var string */ - public const HTML = 'text/html'; - - /** @var string */ - public const PDF = 'application/octet-stream'; - - /** @var string */ - public const TEXT = 'text/plain'; + public const string OCTET_STREAM = "application/octet-stream"; + public const string PDF = "application/pdf"; + public const string POSTSCRIPT = "application/postscript"; + public const string ADOBE_READER_POSTSCRIPT = "application/vnd.adobe-reader-postscript"; + public const string CUPS_PDF = "application/vnd.cups-pdf"; + public const string CUPS_PDF_BANNER = "application/vnd.cups-pdf-banner"; + public const string CUPS_POSTSCRIPT = "application/vnd.cups-postscript"; + public const string CUPS_RASTER = "application/vnd.cups-raster"; + public const string CUPS_RAW = "application/vnd.cups-raw"; + public const string CSHELL = "application/x-cshell"; + public const string CSOURCE = "application/x-csource"; + public const string PERL = "application/x-perl"; + public const string SHELL = "application/x-shell"; + public const string GIF = "image/gif"; + public const string JPEG = "image/jpeg"; + public const string PNG = "image/png"; + public const string PWG_RASTER = "image/pwg-raster"; + public const string TIFF = "image/tiff"; + public const string URF = "image/urf"; + public const string BITMAP = "image/x-bitmap"; + public const string PHOTOCD = "image/x-photocd"; + public const string PORTABLE_ANYMAP = "image/x-portable-anymap"; + public const string PORTABLE_BITMAP = "image/x-portable-bitmap"; + public const string PORTABLE_GRAYMAP = "image/x-portable-graymap"; + public const string PORTABLE_PIXMAP = "image/x-portable-pixmap"; + public const string SGI_RGB = "image/x-sgi-rgb"; + public const string SUN_RASTER = "image/x-sun-raster"; + public const string XBITMAP = "image/x-xbitmap"; + public const string XPIXMAP = "image/x-xpixmap"; + public const string XWINDOWDUMP = "image/x-xwindowdump"; + public const string CSS = "text/css"; + public const string HTML = "text/html"; + public const string PLAIN = "text/plain"; } diff --git a/src/Drivers/Cups/Cups.php b/src/Drivers/Cups/Cups.php index 7e84ee4..999171b 100644 --- a/src/Drivers/Cups/Cups.php +++ b/src/Drivers/Cups/Cups.php @@ -4,129 +4,115 @@ namespace Rawilk\Printing\Drivers\Cups; -use Illuminate\Support\Collection; -use Illuminate\Support\Traits\Macroable; +use Rawilk\Printing\Api\Cups\Cups as CupsApi; +use Rawilk\Printing\Api\Cups\Operation; +use Rawilk\Printing\Api\Cups\Request; +use Rawilk\Printing\Api\Cups\Types\Primitive\Keyword; +use Rawilk\Printing\Api\Cups\Types\Uri; +use Rawilk\Printing\Api\Cups\Version; use Rawilk\Printing\Contracts\Driver; use Rawilk\Printing\Contracts\Printer; use Rawilk\Printing\Contracts\PrintJob; use Rawilk\Printing\Drivers\Cups\Entity\Printer as RawilkPrinter; -use Rawilk\Printing\Drivers\Cups\Support\Client; -use Rawilk\Printing\Exceptions\InvalidDriverConfig; -use Smalot\Cups\Builder\Builder; -use Smalot\Cups\Manager\JobManager; -use Smalot\Cups\Manager\PrinterManager; -use Smalot\Cups\Model\Printer as SmalotPrinter; -use Smalot\Cups\Transport\ResponseParser; +use Rawilk\Printing\Drivers\Cups\PrintTask; class Cups implements Driver { - use Macroable; - - protected Builder $builder; - - protected Client $client; - - protected ResponseParser $responseParser; - - protected PrinterManager $printerManager; - - protected JobManager $jobManager; + private CupsApi $api; public function __construct() { - $this->client = new Client; - $this->responseParser = new ResponseParser; - $this->builder = new Builder(__DIR__ . '/config/'); - } - - public function remoteServer(string $ip, string $username, string $password, int $port = 631): void - { - if (! $username || ! $password) { - throw InvalidDriverConfig::invalid('Remote CUPS server requires a username and password.'); - } - - $this->client = new Client( - $username, - $password, - ['remote_socket' => "tcp://{$ip}:{$port}"] - ); + $this->api = app(CupsApi::class); } public function newPrintTask(): \Rawilk\Printing\Contracts\PrintTask { - return new PrintTask($this->jobManager(), $this->printerManager()); + return new PrintTask(); } public function printer($printerId = null): ?Printer { - $printer = $this->printerManager()->findByUri($printerId); + $request = new Request(); + $request->setVersion(Version::V1_1) + ->setOperation(Operation::GET_PRINTER_ATTRIBUTES) + ->addOperationAttributes(['printer-uri' => new Uri($printerId)]); - if ($printer) { - return new RawilkPrinter($printer, $this->jobManager()); - } - - return null; + return $this->api->makeRequest($request)->getPrinters()->first(); } - /** @return \Illuminate\Support\Collection */ - public function printers(?int $limit = null, ?int $offset = null, ?string $dir = null): Collection + /** + * CUPS doesn't support limit, offset + * + * Printers have a lot of attributes, without the requested attributes filter + * the request will be about 2x slower + * + * @return \Illuminate\Support\Collection + */ + public function printers(?int $limit = null, ?int $offset = null, ?string $dir = null): \Illuminate\Support\Collection { - // TODO: find out if CUPS driver can paginate - $printers = $this->printerManager()->getList(); + $request = new Request(); + $request->setVersion(Version::V1_1) + ->setOperation(Operation::CUPS_GET_PRINTERS); + + $printers = $this->api->makeRequest($request)->getPrinters(); - return collect($printers) - ->map(fn (SmalotPrinter $printer) => new RawilkPrinter($printer, $this->jobManager())) - ->values(); + return $printers->slice($offset, $limit)->values(); } public function printJob($jobId = null): ?PrintJob { - // TODO: Implement printJob() method. - return null; - } + $request = new Request(); + $request->setVersion(Version::V1_1) + ->setOperation(Operation::GET_JOB_ATTRIBUTES) + ->addOperationAttributes( + [ + 'job-uri' => new Uri($jobId), + 'requested-attributes' => new Keyword(['all']), + ] + ); - public function printerPrintJobs($printerId, ?int $limit = null, ?int $offset = null, ?string $dir = null): Collection - { - // TODO: Implement printerPrintJobs() method. - return collect(); + return $this->api->makeRequest($request)->getJobs()->first(); } - public function printerPrintJob($printerId, $jobId): ?PrintJob + /** + * Returns in-progress jobs + */ + public function printerPrintJobs($printerId, ?int $limit = null, ?int $offset = null, ?string $dir = null): \Illuminate\Support\Collection { - // TODO: Implement printerPrintJob() method. - return null; - } + $request = new Request(); + $request->setVersion(Version::V1_1) + ->setOperation(Operation::GET_JOBS) + ->addOperationAttributes( + [ + 'printer-uri' => new Uri($printerId), + 'which-jobs' => new Keyword('not-completed'), + 'requested-attributes' => new Keyword(['job-uri', 'job-state', 'number-of-documents', 'job-name', 'document-format', 'date-time-at-creation', 'job-printer-state-message', 'job-printer-uri']), + ] + ); - /** @return \Illuminate\Support\Collection */ - public function printJobs(?int $limit = null, ?int $offset = null, ?string $dir = null): Collection - { - // TODO: implement printJobs() method. - return collect(); + return $this->api->makeRequest($request)->getJobs(); } - protected function jobManager(): JobManager + public function printerPrintJob($printerId, $jobId): ?PrintJob { - if (! isset($this->jobManager)) { - $this->jobManager = new JobManager( - $this->builder, - $this->client, - $this->responseParser - ); - } - - return $this->jobManager; + return $this->printJob($jobId); } - protected function printerManager(): PrinterManager + /** + * @return \Illuminate\Support\Collection<\Rawilk\Printing\Contracts\PrintJob> + */ + public function printJobs(?int $limit = null, ?int $offset = null, ?string $dir = null): \Illuminate\Support\Collection { - if (! isset($this->printerManager)) { - $this->printerManager = new PrinterManager( - $this->builder, - $this->client, - $this->responseParser - ); - } + $printerUris = $this->printers()->map(fn ($i) => $i->id()); + + $jobs = collect(); + // Make request for each printer... + $printerUris->each( + function ($uri) use ($jobs) { + $jobs->push(...$this->printerPrintJobs($uri)); + } + ); - return $this->printerManager; + return $jobs; } } diff --git a/src/Drivers/Cups/Entity/PrintJob.php b/src/Drivers/Cups/Entity/PrintJob.php index a8c9145..8af1208 100644 --- a/src/Drivers/Cups/Entity/PrintJob.php +++ b/src/Drivers/Cups/Entity/PrintJob.php @@ -5,54 +5,75 @@ namespace Rawilk\Printing\Drivers\Cups\Entity; use Carbon\Carbon; -use Illuminate\Support\Traits\Macroable; +use Illuminate\Contracts\Support\Arrayable; +use JsonSerializable; use Rawilk\Printing\Contracts\PrintJob as PrintJobContract; -use Smalot\Cups\Model\JobInterface; +use Rawilk\Printing\Drivers\Cups\Enum\JobState; -class PrintJob implements PrintJobContract +class PrintJob implements PrintJobContract, Arrayable, JsonSerializable { - use Macroable; + /** + * @param array + */ + protected array $attributes; - public function __construct(protected JobInterface $job, protected ?Printer $printer = null) + /** + * @param array + */ + public function __construct(array $printerAttributes) { + $this->attributes = $printerAttributes; + } + + public function toArray() + { + return [ + 'id' => $this->id(), + 'date' => $this->date(), + 'name' => $this->name(), + 'printerId' => $this->printerId(), + 'printerName' => $this->printerName(), + 'state' => $this->state(), + ]; + } + + public function jsonSerialize(): mixed + { + return $this->toArray(); } public function date(): ?Carbon { - // Not sure if it is possible to retrieve the date. - return null; + return $this->attributes['date-time-at-creation']->value ?? null; } public function id() { - return $this->job->getId(); + // Id serves no purpose, return uri instead? + return $this->attributes['job-uri']->value ?? null; } public function name(): ?string { - return $this->job->getName(); + return $this->attributes['job-name']->value ?? null; } public function printerId() { - if ($this->printer) { - return $this->printer->id(); - } - - return null; + return $this->attributes['job-printer-uri']->value ?? null; } public function printerName(): ?string { - if ($this->printer) { - return $this->printer->name(); + // Extract name from uri + if (preg_match('/printers\/(.*)$/', $this->printerId(), $matches)) { + return $matches[1]; } - return null; } public function state(): ?string { - return $this->job->getState(); + return strtolower(JobState::tryFrom($this->attributes['job-state']->value)->name); } } diff --git a/src/Drivers/Cups/Entity/Printer.php b/src/Drivers/Cups/Entity/Printer.php index 550b6ed..544dc1d 100644 --- a/src/Drivers/Cups/Entity/Printer.php +++ b/src/Drivers/Cups/Entity/Printer.php @@ -5,107 +5,88 @@ namespace Rawilk\Printing\Drivers\Cups\Entity; use Illuminate\Contracts\Support\Arrayable; -use Illuminate\Support\Arr; use Illuminate\Support\Collection; -use Illuminate\Support\Traits\Macroable; use JsonSerializable; -use Rawilk\Printing\Contracts\Printer as PrinterContracts; -use Smalot\Cups\Manager\JobManager; -use Smalot\Cups\Model\JobInterface; -use Smalot\Cups\Model\Printer as SmalotPrinter; +use Rawilk\Printing\Contracts\Printer as PrinterContract; +use Rawilk\Printing\Drivers\Cups\Enum\PrinterState; +use Rawilk\Printing\Facades\Printing; -class Printer implements Arrayable, JsonSerializable, PrinterContracts +class Printer implements Arrayable, JsonSerializable, PrinterContract { - use Macroable; + /** + * @param array + */ + protected array $attributes; - protected array $capabilities; + /** + * @param array + */ + public function __construct(array $printerAttributes) + { + $this->attributes = $printerAttributes; + } - public function __construct(protected SmalotPrinter $printer, protected JobManager $jobManager) + public function toArray() { + return [ + 'id' => $this->id(), + 'name' => $this->name(), + 'description' => $this->description(), + 'online' => $this->isOnline(), + 'status' => $this->status(), + 'trays' => $this->trays(), + 'capabilities' => $this->capabilities(), + ]; } - public function cupsPrinter(): SmalotPrinter + public function jsonSerialize(): mixed { - return $this->printer; + return $this->toArray(); } + /** + * @param array + */ public function capabilities(): array { - if (! isset($this->capabilities)) { - $this->capabilities = $this->printer->getAttributes(); - } - - return $this->capabilities; + return $this->attributes; } public function description(): ?string { - return Arr::get($this->capabilities(), 'printer-info', [])[0] ?? null; + return $this->attributes['printer-info']->value ?? null; } - public function id(): string + public function id() { - return $this->printer->getUri(); + // ID serves no purpose, return uri instead? + $ids = $this->attributes['printer-uri-supported']; + return is_array($ids) ? $ids[0] : $this->attributes['printer-uri-supported']->value; } public function isOnline(): bool { - return strtolower($this->status()) === 'online'; + // Not sure + return true; } public function name(): ?string { - return $this->printer->getName(); + return $this->attributes['printer-name']->value ?? null; } public function status(): string { - return $this->printer->getStatus(); + return strtolower(PrinterState::tryFrom($this->attributes['printer-state']->value)->name); } public function trays(): array { - return Arr::get($this->capabilities(), 'media-source-supported', []); - } - - /** - * @param array $params - * - Possible Params: - * -- limit => int - * -- status => 'completed', 'not-completed' - */ - public function jobs(array $params = []): Collection - { - $supportedStatuses = ['completed', 'not-completed']; - $limit = max(0, Arr::get($params, 'limit', 0)); - $status = Arr::get($params, 'status', 'completed'); - - if (! in_array($status, $supportedStatuses, true)) { - $status = 'completed'; - } - - $jobs = $this->jobManager->getList($this->printer, false, $limit, $status); - - return collect($jobs) - ->map(fn (JobInterface $job) => new PrintJob($job, $this)) - ->values(); + return $this->attributes['media-source-supported']->value ?? []; } - public function toArray(): array + public function jobs(): \Illuminate\Support\Collection { - return [ - 'id' => $this->id(), - 'name' => $this->name(), - 'description' => $this->description(), - 'online' => $this->isOnline(), - 'status' => $this->status(), - 'trays' => $this->trays(), - 'capabilities' => $this->capabilities(), - ]; - } - - public function jsonSerialize(): mixed - { - return $this->toArray(); + return Printing::printerPrintJobs($this->id()); } } diff --git a/src/Drivers/Cups/Enum/JobState.php b/src/Drivers/Cups/Enum/JobState.php new file mode 100644 index 0000000..8008db1 --- /dev/null +++ b/src/Drivers/Cups/Enum/JobState.php @@ -0,0 +1,16 @@ +job = new Job; + $this->api = app(Cups::class); } public function content($content, string $contentType = ContentType::PDF): self { if (! $contentType) { - throw new InvalidSource('Content type is required for the CUPS driver.'); + throw new InvalidSource('Content type is required for the Cups driver.'); } - + $this->contentType = $contentType; parent::content($content); - $this->job->addText($this->content, '', $contentType); - return $this; } - public function file(string $filePath, string $contentType = ContentType::PDF): self + public function orientation(string $value): self { - if (! $contentType) { - throw new InvalidSource('Content type is required for the CUPS driver.'); + switch ($value) { + case 'reverse-portrait': + $orientation = Orientation::REVERSE_PORTRAIT; + break; + case 'reverse-landscape': + $orientation = Orientation::REVERSE_LANDSCAPE; + break; + case 'landscape': + $orientation = Orientation::LANDSCAPE; + break; + case 'portrait': + default: + $orientation = Orientation::PORTRAIT; + break; } - - parent::file($filePath); - - $this->job->addFile($filePath, '', $contentType); - + $this->option('orientation-requested', new Enum($orientation)); return $this; } - public function url(string $url, string $contentType = ContentType::PDF): self + /** + * @param string $key + * @param Type $value + */ + public function option(string $key, $value): self { - if (! $contentType) { - throw new InvalidSource('Content type is required for the CUPS driver.'); - } - - parent::url($url); - - $this->job->addText($this->content, '', $contentType); - + $this->options[$key] = $value; return $this; } - public function printer(PrinterContract|string|null|int $printerId): self + public function copies(int $copies): self { - parent::printer($printerId); - - $this->printer = $printerId instanceof Printer - ? $printerId->cupsPrinter() - : $this->printerManager->findByUri((string) $printerId); - + $this->option('copies', new Integer($copies)); return $this; } public function range($start, $end = null): self { - $range = $start; - - if (! $end && Str::endsWith($range, '-')) { - // If an end page is not set, we will default the end to a really high number - // that hopefully won't ever be exceeded when printing. The reason we have to - // provide an end page is because the library we rely on for CUPS printing - // doesn't allow "printing of all pages", i.e. 1- syntax. - // see: https://github.com/smalot/cups-ipp/issues/7 - $range .= '999'; - } elseif ($end) { - $range = Str::endsWith($range, '-') - ? $range . $end - : "{$range}-{$end}"; + if (!array_key_exists('page-ranges', $this->options)) { + $this->options['page-ranges'] = new RangeOfInteger([[$start, $end]]); + } else { + $this->options['page-ranges']->addRange($start, $end); } - - $this->job->setPageRanges($range); - return $this; } - public function tray($tray): self + /** + * @see \Rawilk\Printing\Drivers\Cups\Sides + */ + public function sides(string $value): self { - if (! empty($tray)) { - $this->job->addAttribute('media-source', $tray); - } - + $this->option('sides', new Keyword($value)); return $this; } - public function copies(int $copies): self + /** + * @param string $tray + */ + public function tray($value): self { - $this->job->setCopies($copies); - + $this->option('media', new Keyword($value)); return $this; } public function send(): PrintJob { - if (! $this->printerId || ! isset($this->printer)) { + $this->ensureValidJob(); + + $request = new Request(); + $request->setVersion(Version::V1_1) + ->setOperation(Operation::PRINT_JOB) + ->addOperationAttributes( + [ + 'printer-uri' => new Uri($this->printerId), + 'document-format' => new MimeMedia($this->contentType), + 'job-name' => new NameWithoutLanguage($this->resolveJobTitle()), + ...$this->options + ] + ) + ->setContent($this->content); + + return $this->api->makeRequest($request)->getJobs()->first(); + } + + protected function ensureValidJob(): void + { + if (!$this->printerId) { throw PrintTaskFailed::missingPrinterId(); } - $this->job->setName($this->resolveJobTitle()); - - foreach ($this->options as $key => $value) { - $this->job->addAttribute($key, $value); + if (!$this->printSource) { + throw PrintTaskFailed::missingSource(); } - if (! $this->job->getPageRanges()) { - // Print all pages if a page range is not specified. - $this->range('1-'); + if (!$this->contentType) { + throw PrintTaskFailed::missingContentType(); } - $success = $this->jobManager->send($this->printer, $this->job); - - if (! $success) { - throw PrintTaskFailed::driverFailed('CUPS print task failed to execute.'); + if (!$this->content) { + throw PrintTaskFailed::noContent(); } - - return new RawilkPrintJob($this->job, new Printer($this->printer, $this->jobManager)); } } diff --git a/src/Drivers/Cups/Sides.php b/src/Drivers/Cups/Sides.php new file mode 100644 index 0000000..db87cc7 --- /dev/null +++ b/src/Drivers/Cups/Sides.php @@ -0,0 +1,13 @@ +username = $username; - } - - if (! is_null($password)) { - $this->password = $password; - } - - if (empty($socketClientOptions['remote_socket'])) { - $socketClientOptions['remote_socket'] = self::SOCKET_URL; - } - - $messageFactory = new GuzzleMessageFactory(); - $socketClient = new SocketHttpClient($messageFactory, $socketClientOptions); - $host = preg_match( - '/unix:\/\//', - $socketClientOptions['remote_socket'] - ) ? 'http://localhost' : $socketClientOptions['remote_socket']; - $this->httpClient = new PluginClient( - $socketClient, - [ - new ErrorPlugin(), - new ContentLengthPlugin(), - new DecoderPlugin(), - new AddHostPlugin(new Uri($host)), - ] - ); - - $this->authType = self::AUTHTYPE_BASIC; - } - - public function setAuthentication(string $username, string $password): self - { - $this->username = $username; - $this->password = $password; - - return $this; - } - - public function setAuthType(string $authType): self - { - $this->authType = $authType; - - return $this; - } - - /** - * (@inheritdoc} - */ - public function sendRequest(RequestInterface $request): ResponseInterface - { - if ($this->username || $this->password) { - switch ($this->authType) { - case self::AUTHTYPE_BASIC: - $pass = base64_encode($this->username . ':' . $this->password); - $authentication = 'Basic ' . $pass; - - break; - - case self::AUTHTYPE_DIGEST: - throw new CupsException('Auth type not supported'); - default: - throw new CupsException('Unknown auth type'); - } - - $request = $request->withHeader('Authorization', $authentication); - } - - return $this->httpClient->sendRequest($request); - } -} diff --git a/src/Factory.php b/src/Factory.php index e02d225..e83c5ce 100644 --- a/src/Factory.php +++ b/src/Factory.php @@ -39,13 +39,7 @@ public function extend(string $driver, Closure $callback): self protected function createCupsDriver(array $config): Driver { - $cups = new Cups; - - if (isset($config['ip'])) { - $cups->remoteServer($config['ip'], $config['username'], $config['password'], $config['port']); - } - - return $cups; + return new Cups(); } protected function createPrintnodeDriver(array $config): Driver diff --git a/src/PrintingServiceProvider.php b/src/PrintingServiceProvider.php index cc3b096..0baef7b 100644 --- a/src/PrintingServiceProvider.php +++ b/src/PrintingServiceProvider.php @@ -4,6 +4,7 @@ namespace Rawilk\Printing; +use Rawilk\Printing\Api\Cups\Cups; use Rawilk\Printing\Api\PrintNode\PrintNode; use Spatie\LaravelPackageTools\Package; use Spatie\LaravelPackageTools\PackageServiceProvider; @@ -29,6 +30,14 @@ public function packageRegistered(): void $this->app->singleton('printing.driver', fn ($app) => $app['printing.factory']->driver()); + $this->app->singleton(Cups::class, fn ($app) => new Cups( + $app['config']['printing']['drivers']['cups']['ip'], + $app['config']['printing']['drivers']['cups']['username'], + $app['config']['printing']['drivers']['cups']['password'], + $app['config']['printing']['drivers']['cups']['port'], + $app['config']['printing']['drivers']['cups']['secure'] ?? false, + )); + $this->app->singleton( Printing::class, fn ($app) => new Printing($app['printing.driver'], $app['config']['printing.default_printer_id']) diff --git a/tests/Feature/Drivers/Cups/Entity/JobTest.php b/tests/Feature/Drivers/Cups/Entity/JobTest.php index 820873d..ca2bccd 100644 --- a/tests/Feature/Drivers/Cups/Entity/JobTest.php +++ b/tests/Feature/Drivers/Cups/Entity/JobTest.php @@ -2,21 +2,8 @@ declare(strict_types=1); -use Rawilk\Printing\Drivers\Cups\Support\Client; -use Smalot\Cups\Builder\Builder; -use Smalot\Cups\Manager\JobManager; -use Smalot\Cups\Transport\ResponseParser; - -beforeEach(function () { - $client = new Client; - $responseParser = new ResponseParser; - $builder = new Builder; - - $this->jobManager = new JobManager($builder, $client, $responseParser); -}); - test('can get the job id', function () { - expect(createCupsJob()->id())->toBe(123456); + expect(createCupsJob()->id())->toBe('localhost:631/jobs/123'); }); test('can get the job name', function () { @@ -24,12 +11,12 @@ }); test('can get the job state', function () { - expect(createCupsJob()->state())->toEqual('success'); + expect(createCupsJob()->state())->toEqual('completed'); }); test('can get the printer name and id', function () { $job = createCupsJob(); expect($job->printerName())->toEqual('printer-name'); - expect($job->printerId())->toEqual('localhost:631'); + expect($job->printerId())->toEqual('localhost:631/printers/printer-name'); }); diff --git a/tests/Feature/Drivers/Cups/Entity/PrinterTest.php b/tests/Feature/Drivers/Cups/Entity/PrinterTest.php index f0fa376..0ac8e26 100644 --- a/tests/Feature/Drivers/Cups/Entity/PrinterTest.php +++ b/tests/Feature/Drivers/Cups/Entity/PrinterTest.php @@ -2,19 +2,6 @@ declare(strict_types=1); -use Rawilk\Printing\Drivers\Cups\Support\Client; -use Smalot\Cups\Builder\Builder; -use Smalot\Cups\Manager\JobManager; -use Smalot\Cups\Transport\ResponseParser; - -beforeEach(function () { - $client = new Client; - $responseParser = new ResponseParser; - $builder = new Builder; - - $this->jobManager = new JobManager($builder, $client, $responseParser); -}); - test('can be cast to array', function () { $printer = createCupsPrinter(); @@ -25,26 +12,27 @@ 'name' => 'printer-name', 'description' => null, 'online' => true, - 'status' => 'online', + 'status' => 'idle', 'trays' => [], - 'capabilities' => [], ]; $this->assertNotEmpty($toArray); - expect($toArray)->toEqual($expected); + expect($toArray)->toMatchArray($expected); }); test('can be cast to json', function () { $printer = createCupsPrinter(); - $json = json_encode($printer); + $json = json_decode(json_encode($printer), true); + $json['capabilities'] = []; + $json = json_encode($json); $expected = json_encode([ 'id' => 'localhost:631', 'name' => 'printer-name', 'description' => null, 'online' => true, - 'status' => 'online', + 'status' => 'idle', 'trays' => [], 'capabilities' => [], ]); @@ -60,18 +48,11 @@ $printer = createCupsPrinter(); expect($printer->isOnline())->toBeTrue(); - expect($printer->status())->toEqual('online'); - - $printer->cupsPrinter()->setStatus('offline'); - - expect($printer->isOnline())->toBeFalse(); + expect($printer->status())->toEqual('idle'); }); test('can get printer description', function () { - $printer = createCupsPrinter(); - - $printer->cupsPrinter()->setAttribute('printer-info', 'Some description'); - + $printer = createCupsPrinter(['printer-info' => new \Rawilk\Printing\Api\Cups\Types\TextWithoutLanguage('Some description')]); expect($printer->description())->toEqual('Some description'); }); @@ -80,10 +61,7 @@ expect($printer->trays())->toHaveCount(0); - // Capabilities are cached after first retrieval, so we'll just use a fresh instance to test this - $printer = createCupsPrinter(); - - $printer->cupsPrinter()->setAttribute('media-source-supported', ['Tray 1']); + $printer = createCupsPrinter(['media-source-supported' => new \Rawilk\Printing\Api\Cups\Types\Primitive\Keyword(['Tray 1'])]); expect($printer->trays())->toHaveCount(1); expect($printer->trays()[0])->toEqual('Tray 1'); diff --git a/tests/Pest.php b/tests/Pest.php index a4fc3e6..de01349 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -2,6 +2,8 @@ declare(strict_types=1); +use Rawilk\Printing\Drivers\Cups\Entity\Printer; +use Rawilk\Printing\Drivers\Cups\Entity\PrintJob; use Rawilk\Printing\Tests\Feature\Api\PrintNode\PrintNodeTestCase; use Rawilk\Printing\Tests\TestCase; @@ -23,20 +25,24 @@ function samplePrintNodeData(string $file): array function createCupsJob(): Rawilk\Printing\Drivers\Cups\Entity\PrintJob { - $cupsJob = new \Smalot\Cups\Model\Job; - $cupsJob->setId(123456) - ->setName('my print job') - ->setState('success'); - - return new \Rawilk\Printing\Drivers\Cups\Entity\PrintJob($cupsJob, createCupsPrinter()); + $cupsJob = new PrintJob([ + 'job-uri' => new Rawilk\Printing\Api\Cups\Types\Uri('localhost:631/jobs/123'), + 'job-printer-uri' => new Rawilk\Printing\Api\Cups\Types\Uri('localhost:631/printers/printer-name'), + 'job-name' => new Rawilk\Printing\Api\Cups\Types\TextWithoutLanguage('my print job'), + 'job-state' => new Rawilk\Printing\Api\Cups\Types\Primitive\Enum(Rawilk\Printing\Drivers\Cups\Enum\JobState::COMPLETED->value), + ]); + + return $cupsJob; } -function createCupsPrinter(): Rawilk\Printing\Drivers\Cups\Entity\Printer +function createCupsPrinter(array $attributes = []): Rawilk\Printing\Drivers\Cups\Entity\Printer { - $cupsPrinter = new \Smalot\Cups\Model\Printer; - $cupsPrinter->setName('printer-name') - ->setUri('localhost:631') - ->setStatus('online'); - - return new \Rawilk\Printing\Drivers\Cups\Entity\Printer($cupsPrinter, test()->jobManager); + $cupsPrinter = new Printer([ + 'printer-name' => new Rawilk\Printing\Api\Cups\Types\TextWithoutLanguage('printer-name'), + 'printer-state' => new Rawilk\Printing\Api\Cups\Types\Primitive\Enum(Rawilk\Printing\Drivers\Cups\Enum\PrinterState::IDLE->value), + 'printer-uri-supported' => new Rawilk\Printing\Api\Cups\Types\TextWithoutLanguage('localhost:631'), + ...$attributes + ]); + + return $cupsPrinter; } From e49bfb9ee0bf89b2e9077ef50508e104986d1c7e Mon Sep 17 00:00:00 2001 From: vatsake Date: Wed, 1 May 2024 17:15:44 +0300 Subject: [PATCH 02/10] . --- composer.json | 152 +++++++++++++++++++++++++------------------------- 1 file changed, 76 insertions(+), 76 deletions(-) diff --git a/composer.json b/composer.json index bbe315b..798b5f0 100644 --- a/composer.json +++ b/composer.json @@ -1,78 +1,78 @@ { - "name": "rawilk/laravel-printing", - "description": "Direct printing for Laravel apps", - "keywords": [ - "rawilk", - "laravel-printing", - "PrintNode", - "CUPS", - "ipp", - "Receipt printing", - "Direct printing", - "Raw printing" - ], - "homepage": "https://github.com/rawilk/laravel-printing", - "license": "MIT", - "authors": [ - { - "name": "Randall Wilk", - "email": "randall@randallwilk.dev", - "homepage": "https://randallwilk.dev", - "role": "Developer" - } - ], - "require": { - "php": "^8.0|^8.1|^8.2|^8.3", - "guzzlehttp/guzzle": "^7.5", - "illuminate/support": "^8.0|^9.0|^10.0|^11.0", - "mike42/escpos-php": "^4.0", - "spatie/laravel-package-tools": "^1.2|^1.13" - }, - "require-dev": { - "laravel/pint": "^1.5", - "mockery/mockery": ">=1.4", - "orchestra/testbench": "^6.0|^7.0|^8.0|^9.0", - "pestphp/pest": "^1.20|^2.34", - "pestphp/pest-plugin-laravel": "^1.0|^2.2", - "php-http/socket-client": "^2.1", - "php-http/message-factory": "^1.1", - "psr/http-message": "1.*", - "psr/http-client": "^1.0", - "spatie/laravel-ray": "^1.0|^1.29" - }, - "autoload": { - "psr-4": { - "Rawilk\\Printing\\": "src" - } - }, - "autoload-dev": { - "psr-4": { - "Rawilk\\Printing\\Tests\\": "tests" - } - }, - "scripts": { - "post-autoload-dump": [ - "@php ./vendor/bin/testbench package:discover --ansi" + "name": "rawilk/laravel-printing", + "description": "Direct printing for Laravel apps", + "keywords": [ + "rawilk", + "laravel-printing", + "PrintNode", + "CUPS", + "ipp", + "Receipt printing", + "Direct printing", + "Raw printing" ], - "test": "vendor/bin/pest -p", - "format": "vendor/bin/pint --dirty" - }, - "config": { - "sort-packages": true, - "allow-plugins": { - "pestphp/pest-plugin": true - } - }, - "extra": { - "laravel": { - "providers": [ - "Rawilk\\Printing\\PrintingServiceProvider" - ], - "aliases": { - "Printing": "Rawilk\\Printing\\Facades\\Printing" - } - } - }, - "minimum-stability": "dev", - "prefer-stable": true -} + "homepage": "https://github.com/rawilk/laravel-printing", + "license": "MIT", + "authors": [ + { + "name": "Randall Wilk", + "email": "randall@randallwilk.dev", + "homepage": "https://randallwilk.dev", + "role": "Developer" + } + ], + "require": { + "php": "^8.0|^8.1|^8.2|^8.3", + "guzzlehttp/guzzle": "^7.5", + "illuminate/support": "^8.0|^9.0|^10.0|^11.0", + "mike42/escpos-php": "^4.0", + "spatie/laravel-package-tools": "^1.2|^1.13" + }, + "require-dev": { + "laravel/pint": "^1.5", + "mockery/mockery": ">=1.4", + "orchestra/testbench": "^6.0|^7.0|^8.0|^9.0", + "pestphp/pest": "^1.20|^2.34", + "pestphp/pest-plugin-laravel": "^1.0|^2.2", + "php-http/socket-client": "^2.1", + "php-http/message-factory": "^1.1", + "psr/http-message": "1.*", + "psr/http-client": "^1.0", + "spatie/laravel-ray": "^1.0|^1.29" + }, + "autoload": { + "psr-4": { + "Rawilk\\Printing\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "Rawilk\\Printing\\Tests\\": "tests" + } + }, + "scripts": { + "post-autoload-dump": [ + "@php ./vendor/bin/testbench package:discover --ansi" + ], + "test": "vendor/bin/pest -p", + "format": "vendor/bin/pint --dirty" + }, + "config": { + "sort-packages": true, + "allow-plugins": { + "pestphp/pest-plugin": true + } + }, + "extra": { + "laravel": { + "providers": [ + "Rawilk\\Printing\\PrintingServiceProvider" + ], + "aliases": { + "Printing": "Rawilk\\Printing\\Facades\\Printing" + } + } + }, + "minimum-stability": "dev", + "prefer-stable": true +} \ No newline at end of file From 2e388688fda07dd7769f917fd87f7aca2573dabb Mon Sep 17 00:00:00 2001 From: vatsake Date: Wed, 1 May 2024 17:20:38 +0300 Subject: [PATCH 03/10] fix debug line --- src/Drivers/Cups/Cups.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Drivers/Cups/Cups.php b/src/Drivers/Cups/Cups.php index 999171b..0839d2c 100644 --- a/src/Drivers/Cups/Cups.php +++ b/src/Drivers/Cups/Cups.php @@ -67,7 +67,7 @@ public function printJob($jobId = null): ?PrintJob ->addOperationAttributes( [ 'job-uri' => new Uri($jobId), - 'requested-attributes' => new Keyword(['all']), + 'requested-attributes' => new Keyword(['job-uri', 'job-state', 'number-of-documents', 'job-name', 'document-format', 'date-time-at-creation', 'job-printer-state-message', 'job-printer-uri']), ] ); From d5aa8c5d1693a3d6125bf60e0b095269db721ce1 Mon Sep 17 00:00:00 2001 From: vatsake Date: Wed, 1 May 2024 17:29:57 +0300 Subject: [PATCH 04/10] auth fix --- src/Api/Cups/Cups.php | 8 +++++++- src/Printing.php | 8 ++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/Api/Cups/Cups.php b/src/Api/Cups/Cups.php index af9b19a..6ee5f6a 100644 --- a/src/Api/Cups/Cups.php +++ b/src/Api/Cups/Cups.php @@ -25,7 +25,13 @@ public function __construct( */ public function makeRequest(Request $request): Response { - $http = Http::withBody($request->encode())->withHeaders( + $http = Http::withBody($request->encode()); + + if ($this->username || $this->password) { + $http->withBasicAuth($this->username, $this->password); + } + + $http = $http->withHeaders( [ "Content-Type" => "application/ipp" ] diff --git a/src/Printing.php b/src/Printing.php index b787232..894ba85 100644 --- a/src/Printing.php +++ b/src/Printing.php @@ -47,11 +47,11 @@ public function newPrintTask(): Contracts\PrintTask public function printer($printerId = null): ?Printer { - try { + //try { $printer = $this->driver->printer($printerId); - } catch (Throwable) { - $printer = null; - } + //} catch (Throwable $e) { + // $printer = null; + //} $this->resetDriver(); From af83be045ab77af6912ff5edcdd11a849de81c8a Mon Sep 17 00:00:00 2001 From: vatsake Date: Wed, 1 May 2024 17:44:29 +0300 Subject: [PATCH 05/10] fix-dpi --- src/Api/Cups/Types/Resolution.php | 4 ++-- src/Printing.php | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Api/Cups/Types/Resolution.php b/src/Api/Cups/Types/Resolution.php index cd24d1d..fe71fb3 100644 --- a/src/Api/Cups/Types/Resolution.php +++ b/src/Api/Cups/Types/Resolution.php @@ -27,9 +27,9 @@ public function encode(): string } - public static function decode(string $binary, ?int $length = null): mixed + public static function fromBinary(string $binary, ?int $length = null): self { $value = unpack('Np/Np2/cu', $binary); - return $value['p'] . 'x' . $value['p2'] . static::$unitMap[$value['u']]; + return new static($value['p'] . 'x' . $value['p2'] . static::$unitMap[$value['u']]); } } diff --git a/src/Printing.php b/src/Printing.php index 894ba85..867de53 100644 --- a/src/Printing.php +++ b/src/Printing.php @@ -47,11 +47,11 @@ public function newPrintTask(): Contracts\PrintTask public function printer($printerId = null): ?Printer { - //try { + try { $printer = $this->driver->printer($printerId); - //} catch (Throwable $e) { - // $printer = null; - //} + } catch (Throwable $e) { + $printer = null; + } $this->resetDriver(); From b5609b7463844e7cc190996d9b8f03bcd815f19a Mon Sep 17 00:00:00 2001 From: vatsake Date: Thu, 2 May 2024 09:57:47 +0300 Subject: [PATCH 06/10] made array of attributes encoding/decoding better --- src/Api/Cups/AttributeGroup.php | 20 ++++++++------ src/Api/Cups/Request.php | 4 +-- src/Api/Cups/Response.php | 13 ++++----- src/Api/Cups/Types/RangeOfInteger.php | 38 ++++++++++----------------- src/Drivers/Cups/Cups.php | 34 +++++++++++++++++------- src/Drivers/Cups/PrintTask.php | 9 ++++--- 6 files changed, 63 insertions(+), 55 deletions(-) diff --git a/src/Api/Cups/AttributeGroup.php b/src/Api/Cups/AttributeGroup.php index 33944c0..1e90782 100644 --- a/src/Api/Cups/AttributeGroup.php +++ b/src/Api/Cups/AttributeGroup.php @@ -14,7 +14,7 @@ abstract class AttributeGroup protected int $tag; /** - * @var array + * @var array> */ protected array $attributes = []; @@ -29,7 +29,7 @@ public function __construct(array $attributes = []) } /** - * @var array + * @var array> */ public function getAttributes() { @@ -40,7 +40,7 @@ public function encode(): string { $binary = pack('c', $this->tag); foreach ($this->attributes as $name => $value) { - if (gettype($value->value) === 'array') { + if (gettype($value) === 'array') { $binary .= $this->handleArrayEncode($name, $value); continue; } @@ -56,23 +56,27 @@ public function encode(): string /** * If attribute is an array, the attribute name after the first element is empty + * @param string $name + * @param array $values */ - private function handleArrayEncode(string $name, \Rawilk\Printing\Api\Cups\Type $value): string + private function handleArrayEncode(string $name, array $values): string { $str = ''; - for ($i = 0; $i < sizeof($value->value); $i++) { + if (get_class($values[0]) === \Rawilk\Printing\Api\Cups\Types\RangeOfInteger::class) { + \Rawilk\Printing\Api\Cups\Types\RangeOfInteger::checkOverlaps($values); + } + for ($i = 0; $i < sizeof($values); $i++) { $_name = $name; if ($i !== 0) { $_name = ''; } $nameLen = strlen($_name); - $str .= pack('c', $value->getTag()); // Value tag + $str .= pack('c', $values[$i]->getTag()); // Value tag $str .= pack('n', $nameLen); // Attribute key length $str .= pack('a' . $nameLen, $_name); // Attribute key - $class = $value::class; - $str .= (new $class($value->value[$i]))->encode(); + $str .= $values[$i]->encode(); } return $str; } diff --git a/src/Api/Cups/Request.php b/src/Api/Cups/Request.php index c0319f6..e8e3218 100644 --- a/src/Api/Cups/Request.php +++ b/src/Api/Cups/Request.php @@ -62,7 +62,7 @@ public function setRequestId(int $requestId) } /** - * @param array $attributes + * @param array> $attributes */ public function addOperationAttributes(array $attributes) { @@ -71,7 +71,7 @@ public function addOperationAttributes(array $attributes) } /** - * @param array $attributes + * @param array $attributes */ public function addJobAttributes(array $attributes) { diff --git a/src/Api/Cups/Response.php b/src/Api/Cups/Response.php index 99ac4ef..8ca3d15 100644 --- a/src/Api/Cups/Response.php +++ b/src/Api/Cups/Response.php @@ -84,17 +84,14 @@ private function extractAttributes(string $binary, int &$offset, mixed &$nextTag // Array of values if ($attrName === '') { - $lastAttr = $attributes[array_key_last($attributes)]; + $index = array_key_last($attributes); + $lastAttr = $attributes[$index]; - if ($typeTag !== TypeTag::RANGEOFINTEGER->value && gettype($lastAttr->value) !== 'array') { - $lastAttr->value = [$lastAttr->value]; + if (!is_array($lastAttr)) { + $attributes[$index] = [$lastAttr]; } - if ($typeTag == TypeTag::RANGEOFINTEGER->value) { - $lastAttr->value[] = $attribute->value[0]; - } else { - $lastAttr->value[] = $attribute->value; - } + $attributes[$index][] = $attribute; } else { $attributes[$attrName] = $attribute; } diff --git a/src/Api/Cups/Types/RangeOfInteger.php b/src/Api/Cups/Types/RangeOfInteger.php index 7b62886..873051d 100644 --- a/src/Api/Cups/Types/RangeOfInteger.php +++ b/src/Api/Cups/Types/RangeOfInteger.php @@ -4,6 +4,7 @@ namespace Rawilk\Printing\Api\Cups\Types; +use Rawilk\Printing\Api\Cups\Exceptions\RangeOverlap; use Rawilk\Printing\Api\Cups\Type; use Rawilk\Printing\Api\Cups\TypeTag; @@ -12,12 +13,10 @@ class RangeOfInteger extends Type protected int $tag = TypeTag::RANGEOFINTEGER->value; /** - * @param array|int[] $value + * @param int[] $value - Array of 2 integers */ public function __construct(public mixed $value) { - parent::__construct($value); - $this->checkOverlaps(); } public function encode(): string @@ -28,36 +27,27 @@ public function encode(): string public static function fromBinary(string $binary, ?int $length = null): self { $value = unpack('Nl/Nu', $binary); - return new static([[$value['l'], $value['u']]]); + return new static([$value['l'], $value['u']]); } - public function addRange($lower, $upper) - { - $this->value[] = [$lower, $upper]; - $this->checkOverlaps(); - } - - private function sortValues() + /** + * Sorts and checks the array for overlaps + * + * @param array $values + * @throws RangeOverlap + */ + public static function checkOverlaps(array &$values) { usort( - $this->value, + $values, function ($a, $b) { - return $a[0] - $b[0]; + return $a->value[0] - $b->value[0]; } ); - } - - private function checkOverlaps() - { - if (gettype($this->value[0]) !== 'array') { - return; - } - $this->sortValues(); - $ranges = $this->value; - $count = count($ranges); + $count = count($values); for ($i = 0; $i < $count - 1; $i++) { - if ($ranges[$i][1] >= $ranges[$i + 1][0]) { + if ($values[$i]->value[1] >= $values[$i + 1]->value[0]) { throw new \Rawilk\Printing\Api\Cups\Exceptions\RangeOverlap('Range overlap is not allowed!'); } } diff --git a/src/Drivers/Cups/Cups.php b/src/Drivers/Cups/Cups.php index 0839d2c..e765525 100644 --- a/src/Drivers/Cups/Cups.php +++ b/src/Drivers/Cups/Cups.php @@ -64,12 +64,19 @@ public function printJob($jobId = null): ?PrintJob $request = new Request(); $request->setVersion(Version::V1_1) ->setOperation(Operation::GET_JOB_ATTRIBUTES) - ->addOperationAttributes( - [ + ->addOperationAttributes([ 'job-uri' => new Uri($jobId), - 'requested-attributes' => new Keyword(['job-uri', 'job-state', 'number-of-documents', 'job-name', 'document-format', 'date-time-at-creation', 'job-printer-state-message', 'job-printer-uri']), - ] - ); + 'requested-attributes' => [ + new Keyword('job-uri'), + new Keyword('job-state'), + new Keyword('number-of-documents'), + new Keyword('job-name'), + new Keyword('document-format'), + new Keyword('date-time-at-creation'), + new Keyword('job-printer-state-message'), + new Keyword('job-printer-uri') + ], + ]); return $this->api->makeRequest($request)->getJobs()->first(); } @@ -82,13 +89,20 @@ public function printerPrintJobs($printerId, ?int $limit = null, ?int $offset = $request = new Request(); $request->setVersion(Version::V1_1) ->setOperation(Operation::GET_JOBS) - ->addOperationAttributes( - [ + ->addOperationAttributes([ 'printer-uri' => new Uri($printerId), 'which-jobs' => new Keyword('not-completed'), - 'requested-attributes' => new Keyword(['job-uri', 'job-state', 'number-of-documents', 'job-name', 'document-format', 'date-time-at-creation', 'job-printer-state-message', 'job-printer-uri']), - ] - ); + 'requested-attributes' => [ + new Keyword('job-uri'), + new Keyword('job-state'), + new Keyword('number-of-documents'), + new Keyword('job-name'), + new Keyword('document-format'), + new Keyword('date-time-at-creation'), + new Keyword('job-printer-state-message'), + new Keyword('job-printer-uri') + ], + ]); return $this->api->makeRequest($request)->getJobs(); } diff --git a/src/Drivers/Cups/PrintTask.php b/src/Drivers/Cups/PrintTask.php index 2cd2c5b..d183548 100644 --- a/src/Drivers/Cups/PrintTask.php +++ b/src/Drivers/Cups/PrintTask.php @@ -67,7 +67,7 @@ public function orientation(string $value): self /** * @param string $key - * @param Type $value + * @param Type|Type[] $value */ public function option(string $key, $value): self { @@ -84,9 +84,12 @@ public function copies(int $copies): self public function range($start, $end = null): self { if (!array_key_exists('page-ranges', $this->options)) { - $this->options['page-ranges'] = new RangeOfInteger([[$start, $end]]); + $this->options['page-ranges'] = new RangeOfInteger([$start, $end]); } else { - $this->options['page-ranges']->addRange($start, $end); + if (!is_array($this->options['page-ranges'])) { + $this->options['page-ranges'] = [$this->options['page-ranges']]; + } + $this->options['page-ranges'][] = new RangeOfInteger([$start, $end]); } return $this; } From b172d53772b2830aac64c887101fdf4331ecb64d Mon Sep 17 00:00:00 2001 From: vatsake Date: Thu, 2 May 2024 11:33:17 +0300 Subject: [PATCH 07/10] print task options to job attributes --- src/Api/Cups/Types/Primitive/Integer.php | 2 +- src/Drivers/Cups/PrintTask.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Api/Cups/Types/Primitive/Integer.php b/src/Api/Cups/Types/Primitive/Integer.php index 5d72349..52cb0bf 100644 --- a/src/Api/Cups/Types/Primitive/Integer.php +++ b/src/Api/Cups/Types/Primitive/Integer.php @@ -13,7 +13,7 @@ class Integer extends Type public function encode(): string { - return pack('n', strlen($this->value)) . pack('N', $this->value); + return pack('n', 4) . pack('N', $this->value); } public static function fromBinary(string $binary, ?int $length = null): self diff --git a/src/Drivers/Cups/PrintTask.php b/src/Drivers/Cups/PrintTask.php index d183548..b03169f 100644 --- a/src/Drivers/Cups/PrintTask.php +++ b/src/Drivers/Cups/PrintTask.php @@ -124,9 +124,9 @@ public function send(): PrintJob 'printer-uri' => new Uri($this->printerId), 'document-format' => new MimeMedia($this->contentType), 'job-name' => new NameWithoutLanguage($this->resolveJobTitle()), - ...$this->options ] ) + ->addJobAttributes($this->options) ->setContent($this->content); return $this->api->makeRequest($request)->getJobs()->first(); From 1cfb190baac8cc10cfd1a347b6a2c56f8ccb7022 Mon Sep 17 00:00:00 2001 From: vatsake Date: Fri, 3 May 2024 11:27:41 +0300 Subject: [PATCH 08/10] changed decoding logic, set version to 2.1 --- src/Api/Cups/AttributeGroup.php | 11 ++- src/Api/Cups/Exceptions/TypeNotSpecified.php | 15 ++++ src/Api/Cups/Response.php | 15 +--- src/Api/Cups/Type.php | 27 +++++++- src/Api/Cups/TypeTag.php | 9 ++- src/Api/Cups/Types/Collection.php | 72 ++++++++++++++++++++ src/Api/Cups/Types/DateTime.php | 21 ++++-- src/Api/Cups/Types/Member.php | 48 +++++++++++++ src/Api/Cups/Types/Primitive/Boolean.php | 12 +++- src/Api/Cups/Types/Primitive/Enum.php | 12 +++- src/Api/Cups/Types/Primitive/Integer.php | 12 +++- src/Api/Cups/Types/Primitive/Keyword.php | 12 +++- src/Api/Cups/Types/Primitive/NoValue.php | 7 +- src/Api/Cups/Types/Primitive/OctetString.php | 13 +--- src/Api/Cups/Types/Primitive/Text.php | 12 +++- src/Api/Cups/Types/Primitive/Unknown.php | 7 +- src/Api/Cups/Types/RangeOfInteger.php | 13 +++- src/Api/Cups/Types/Resolution.php | 14 ++-- src/Api/Cups/Version.php | 2 + src/Drivers/Cups/Cups.php | 8 +-- src/Drivers/Cups/PrintTask.php | 9 --- 21 files changed, 282 insertions(+), 69 deletions(-) create mode 100644 src/Api/Cups/Exceptions/TypeNotSpecified.php create mode 100644 src/Api/Cups/Types/Collection.php create mode 100644 src/Api/Cups/Types/Member.php diff --git a/src/Api/Cups/AttributeGroup.php b/src/Api/Cups/AttributeGroup.php index 1e90782..2746d40 100644 --- a/src/Api/Cups/AttributeGroup.php +++ b/src/Api/Cups/AttributeGroup.php @@ -4,6 +4,8 @@ namespace Rawilk\Printing\Api\Cups; +use Rawilk\Printing\Api\Cups\Exceptions\TypeNotSpecified; + abstract class AttributeGroup { /** @@ -44,11 +46,16 @@ public function encode(): string $binary .= $this->handleArrayEncode($name, $value); continue; } - $nameLen = strlen($name); + if (!$value instanceof Type) { + throw new TypeNotSpecified('Attribute value has to be of type ' . Type::class); + } + $nameLen = strlen($name); $binary .= pack('c', $value->getTag()); + $binary .= pack('n', $nameLen); // Attribute key length $binary .= pack('a' . $nameLen, $name); // Attribute key + $binary .= $value->encode(); // Attribute value (with length) } return $binary; @@ -62,7 +69,7 @@ public function encode(): string private function handleArrayEncode(string $name, array $values): string { $str = ''; - if (get_class($values[0]) === \Rawilk\Printing\Api\Cups\Types\RangeOfInteger::class) { + if ($values[0] instanceof \Rawilk\Printing\Api\Cups\Types\RangeOfInteger) { \Rawilk\Printing\Api\Cups\Types\RangeOfInteger::checkOverlaps($values); } for ($i = 0; $i < sizeof($values); $i++) { diff --git a/src/Api/Cups/Exceptions/TypeNotSpecified.php b/src/Api/Cups/Exceptions/TypeNotSpecified.php new file mode 100644 index 0000000..03e1e87 --- /dev/null +++ b/src/Api/Cups/Exceptions/TypeNotSpecified.php @@ -0,0 +1,15 @@ +getClass(); - $attribute = $typeClass::fromBinary(substr($binary, $offset, $valueLen), $valueLen); - $offset += $valueLen; + [$attrName, $attribute] = $typeClass::fromBinary($binary, $offset); + // Array of values if ($attrName === '') { diff --git a/src/Api/Cups/Type.php b/src/Api/Cups/Type.php index a15af23..a309451 100644 --- a/src/Api/Cups/Type.php +++ b/src/Api/Cups/Type.php @@ -14,7 +14,32 @@ public function __construct(public mixed $value) protected int $tag; - abstract public static function fromBinary(string $binary, ?int $length = null): self; + /** + * Returns attribute from binary and increments offset + * + * @param string $binary + * @param int $offset + * @return [string, Type] + */ + abstract public static function fromBinary(string $binary, int &$offset): array; + + /** + * Returns name from binary and increments offset + * + * @param string $binary + * @param int $offset + * @return string attribute name + */ + protected static function nameFromBinary(string $binary, int &$offset): string + { + $nameLen = (unpack('n', $binary, $offset))[1]; + $offset += 2; + + $attrName = unpack('a' . $nameLen, $binary, $offset)[1]; + $offset += $nameLen; + + return $attrName; + } /** * Returns value length and value in binary diff --git a/src/Api/Cups/TypeTag.php b/src/Api/Cups/TypeTag.php index ad16ffa..c0ed6aa 100644 --- a/src/Api/Cups/TypeTag.php +++ b/src/Api/Cups/TypeTag.php @@ -4,13 +4,15 @@ namespace Rawilk\Printing\Api\Cups; +use Rawilk\Printing\Api\Cups\Exceptions\UnknownType; use Rawilk\Printing\Api\Cups\Types\Charset; -use Rawilk\Printing\Api\Cups\Types\Contracts\Formattable; use Rawilk\Printing\Api\Cups\Types\DateTime; use Rawilk\Printing\Api\Cups\Types\MimeMedia; use Rawilk\Printing\Api\Cups\Types\NameWithoutLanguage; use Rawilk\Printing\Api\Cups\Types\NaturalLanguage; use Rawilk\Printing\Api\Cups\Types\Primitive\Boolean; +use Rawilk\Printing\Api\Cups\Types\Collection; +use Rawilk\Printing\Api\Cups\Types\Member; use Rawilk\Printing\Api\Cups\Types\Primitive\Enum; use Rawilk\Printing\Api\Cups\Types\Primitive\Integer; use Rawilk\Printing\Api\Cups\Types\Primitive\Keyword; @@ -37,6 +39,7 @@ enum TypeTag: int case COLLECTION = 0x34; case TEXTWITHLANGUAGE = 0x35; case NAMEWITHLANGUAGE = 0x36; + case COLLECTION_END = 0x37; case TEXTWITHOUTLANGUAGE = 0x41; case NAMEWITHOUTLANGUAGE = 0x42; case KEYWORD = 0x44; @@ -45,6 +48,7 @@ enum TypeTag: int case CHARSET = 0x47; case NATURALLANGUAGE = 0x48; case MIMEMEDIATYPE = 0x49; + case MEMBER = 0x4a; case NAME = 0x0008; case STATUSCODE = 0x000D; case TEXT = 0x000E; @@ -68,6 +72,9 @@ public function getClass(): string self::MIMEMEDIATYPE->value => MimeMedia::class, self::RESOLUTION->value => Resolution::class, self::RANGEOFINTEGER->value => RangeOfInteger::class, + self::COLLECTION->value => Collection::class, + self::MEMBER->value => Member::class, + default => throw new UnknownType('Unknown type') }; } } diff --git a/src/Api/Cups/Types/Collection.php b/src/Api/Cups/Types/Collection.php new file mode 100644 index 0000000..3c1a318 --- /dev/null +++ b/src/Api/Cups/Types/Collection.php @@ -0,0 +1,72 @@ +value; + + // Collection has an end tag + protected int $endTag = TypeTag::COLLECTION_END->value; + + /** + * @param array $value - Array of members + */ + public function __construct(public mixed $value) + { + } + + public function encode(): string + { + $binary = pack('n', 0); // Value length is 0 + + foreach ($this->value as $key => $value) { + $binary .= pack('c', TypeTag::MEMBER->value); + $binary .= pack('n', 0); // Member name length is 0 + + $binary .= pack('n', strlen($key)); + $binary .= pack('a' . strlen($key), $key); + + $binary .= $value->encode(); + } + + // Collection has an end tag (with name, value) + $binary .= pack('c', $this->endTag); + $binary .= pack('n', 0); // End tag name length is 0 + $binary .= pack('n', 0); // End tag value length is 0 + + return $binary; + } + + public static function fromBinary(string $binary, int &$offset): array + { + $attrName = self::nameFromBinary($binary, $offset); + $offset += 2; // Value length + + $members = []; + while (unpack("ctag", $binary, $offset)['tag'] === TypeTag::MEMBER->value) { + $nextTag = (unpack("ctag", $binary, $offset))['tag']; + $offset++; + + $type = TypeTag::tryFrom($nextTag); + $typeClass = $type->getClass(); + + [$name, $value] = $typeClass::fromBinary($binary, $offset); + $members[$name] = $value; + } + + // Collection end tags + $offset++; // 0x37 + $offset += 4; // Name, value length + + return [$attrName, new static($members)]; + } +} diff --git a/src/Api/Cups/Types/DateTime.php b/src/Api/Cups/Types/DateTime.php index a327ae6..1c85ede 100644 --- a/src/Api/Cups/Types/DateTime.php +++ b/src/Api/Cups/Types/DateTime.php @@ -34,13 +34,19 @@ public function encode(): string . pack('c', self::unpad($matches[3])); } - public static function fromBinary(string $binary, ?int $length = null): self + public static function fromBinary(string $binary, int &$offset): array { - $data = unpack('nY/cm/cd/cH/ci/cs/cfff/aUTCSym/cUTCm/cUTCs', $binary); - return new static( - Carbon::createFromFormat( - 'YmdHisO', - $data['Y'] + $attrName = self::nameFromBinary($binary, $offset); + + $valueLen = (unpack('n', $binary, $offset))[1]; + $offset += 2; + + $data = unpack('nY/cm/cd/cH/ci/cs/cfff/aUTCSym/cUTCm/cUTCs', $binary, $offset); + $offset += $valueLen; + + $value = Carbon::createFromFormat( + 'YmdHisO', + $data['Y'] . str_pad((string) $data['m'], 2, '0', STR_PAD_LEFT) . str_pad((string) $data['d'], 2, '0', STR_PAD_LEFT) . str_pad((string) $data['H'], 2, '0', STR_PAD_LEFT) @@ -49,8 +55,9 @@ public static function fromBinary(string $binary, ?int $length = null): self . $data['UTCSym'] . str_pad((string)$data['UTCm'], 2, '0', STR_PAD_LEFT) . str_pad((string)$data['UTCs'], 2, '0', STR_PAD_LEFT) - ) ); + + return [$attrName, new static($value)]; } private static function unpad(string $str) diff --git a/src/Api/Cups/Types/Member.php b/src/Api/Cups/Types/Member.php new file mode 100644 index 0000000..7a191bd --- /dev/null +++ b/src/Api/Cups/Types/Member.php @@ -0,0 +1,48 @@ +value; + + public function encode(): string + { + $binary = pack('c', $this->value->getTag()); + $binary .= pack('n', 0); // Name length is 0 + $binary .= $this->value->encode(); + return $binary; + } + + /** + * @see https://datatracker.ietf.org/doc/html/rfc3382#section-7.2 + */ + public static function fromBinary(string $binary, int &$offset): array + { + // Name is empty + self::nameFromBinary($binary, $offset); + + $valueLen = (unpack('n', $binary, $offset))[1]; + $offset += 2; + + // This will be the attribute name + $value = unpack('a' . $valueLen, $binary, $offset)[1]; + $offset += $valueLen; + + $nextTag = (unpack("ctag", $binary, $offset))['tag']; + $offset++; + + $type = TypeTag::tryFrom($nextTag); + $typeClass = $type->getClass(); + + // This will be the value + $value2 = $typeClass::fromBinary($binary, $offset)[1]; + + return [$value, new static($value2)]; + } +} diff --git a/src/Api/Cups/Types/Primitive/Boolean.php b/src/Api/Cups/Types/Primitive/Boolean.php index 6b3ea41..eb8d9aa 100644 --- a/src/Api/Cups/Types/Primitive/Boolean.php +++ b/src/Api/Cups/Types/Primitive/Boolean.php @@ -16,8 +16,16 @@ public function encode(): string return pack('n', 1) . pack('c', intval($this->value)); } - public static function fromBinary(string $binary, ?int $length = null): self + public static function fromBinary(string $binary, int &$offset): array { - return new static((bool) unpack('c', $binary)[1]); + $attrName = self::nameFromBinary($binary, $offset); + + $valueLen = (unpack('n', $binary, $offset))[1]; + $offset += 2; + + $value = (bool) unpack('c', $binary, $offset)[1]; + $offset += $valueLen; + + return [$attrName, new static($value)]; } } diff --git a/src/Api/Cups/Types/Primitive/Enum.php b/src/Api/Cups/Types/Primitive/Enum.php index 75765f9..e37cef8 100644 --- a/src/Api/Cups/Types/Primitive/Enum.php +++ b/src/Api/Cups/Types/Primitive/Enum.php @@ -16,8 +16,16 @@ public function encode(): string return pack('n', 4) . pack('N', $this->value); } - public static function fromBinary(string $binary, ?int $length = null): self + public static function fromBinary(string $binary, int &$offset): array { - return new static(unpack('N', $binary)[1]); + $attrName = self::nameFromBinary($binary, $offset); + + $valueLen = (unpack('n', $binary, $offset))[1]; + $offset += 2; + + $value = unpack('N', $binary, $offset)[1]; + $offset += $valueLen; + + return [$attrName, new static($value)]; } } diff --git a/src/Api/Cups/Types/Primitive/Integer.php b/src/Api/Cups/Types/Primitive/Integer.php index 52cb0bf..8c837a3 100644 --- a/src/Api/Cups/Types/Primitive/Integer.php +++ b/src/Api/Cups/Types/Primitive/Integer.php @@ -16,8 +16,16 @@ public function encode(): string return pack('n', 4) . pack('N', $this->value); } - public static function fromBinary(string $binary, ?int $length = null): self + public static function fromBinary(string $binary, int &$offset): array { - return new static(unpack('N', $binary)[1]); + $attrName = self::nameFromBinary($binary, $offset); + + $valueLen = (unpack('n', $binary, $offset))[1]; + $offset += 2; + + $value = unpack('N', $binary, $offset)[1]; + $offset += $valueLen; + + return [$attrName, new static($value)]; } } diff --git a/src/Api/Cups/Types/Primitive/Keyword.php b/src/Api/Cups/Types/Primitive/Keyword.php index 6dd7bb2..8cf14ad 100644 --- a/src/Api/Cups/Types/Primitive/Keyword.php +++ b/src/Api/Cups/Types/Primitive/Keyword.php @@ -16,8 +16,16 @@ public function encode(): string return pack('n', strlen($this->value)) . pack('a' . strlen($this->value), $this->value); } - public static function fromBinary(string $binary, ?int $length = null): self + public static function fromBinary(string $binary, int &$offset): array { - return new static(unpack('a' . $length, $binary)[1]); + $attrName = self::nameFromBinary($binary, $offset); + + $valueLen = (unpack('n', $binary, $offset))[1]; + $offset += 2; + + $value = unpack('a' . $valueLen, $binary, $offset)[1]; + $offset += $valueLen; + + return [$attrName, new static($value)]; } } diff --git a/src/Api/Cups/Types/Primitive/NoValue.php b/src/Api/Cups/Types/Primitive/NoValue.php index 001b7e0..497eb7c 100644 --- a/src/Api/Cups/Types/Primitive/NoValue.php +++ b/src/Api/Cups/Types/Primitive/NoValue.php @@ -16,8 +16,11 @@ public function encode(): string return pack('n', 0) . ''; } - public static function fromBinary(string $binary, ?int $length = null): self + public static function fromBinary(string $binary, int &$offset): array { - return new static(null); + $attrName = self::nameFromBinary($binary, $offset); + $offset += 2; // Value length + + return [$attrName, new static(null)]; } } diff --git a/src/Api/Cups/Types/Primitive/OctetString.php b/src/Api/Cups/Types/Primitive/OctetString.php index 37c344e..0e943c6 100644 --- a/src/Api/Cups/Types/Primitive/OctetString.php +++ b/src/Api/Cups/Types/Primitive/OctetString.php @@ -4,20 +4,9 @@ namespace Rawilk\Printing\Api\Cups\Types\Primitive; -use Rawilk\Printing\Api\Cups\Type; use Rawilk\Printing\Api\Cups\TypeTag; -class OctetString extends Type +class OctetString extends Text { protected int $tag = TypeTag::OCTETSTRING->value; - - public function encode(): string - { - return pack('a', strlen($this->value)) . pack('a' . strlen($this->value), $this->value); - } - - public static function fromBinary(string $binary, ?int $length = null): self - { - return new static(unpack('a' . $length, $binary)[1]); - } } diff --git a/src/Api/Cups/Types/Primitive/Text.php b/src/Api/Cups/Types/Primitive/Text.php index f9e84bd..7ee614f 100644 --- a/src/Api/Cups/Types/Primitive/Text.php +++ b/src/Api/Cups/Types/Primitive/Text.php @@ -16,8 +16,16 @@ public function encode(): string return pack('n', strlen($this->value)) . pack('a' . strlen($this->value), $this->value); } - public static function fromBinary(string $binary, ?int $length = null): self + public static function fromBinary(string $binary, int &$offset): array { - return new static(unpack('a' . $length, $binary)[1]); + $attrName = self::nameFromBinary($binary, $offset); + + $valueLen = (unpack('n', $binary, $offset))[1]; + $offset += 2; + + $value = unpack('a' . $valueLen, $binary, $offset)[1]; + $offset += $valueLen; + + return [$attrName, new static($value)]; } } diff --git a/src/Api/Cups/Types/Primitive/Unknown.php b/src/Api/Cups/Types/Primitive/Unknown.php index 9ac5a77..505063f 100644 --- a/src/Api/Cups/Types/Primitive/Unknown.php +++ b/src/Api/Cups/Types/Primitive/Unknown.php @@ -16,8 +16,11 @@ public function encode(): string return pack('n', 0) . ''; } - public static function fromBinary(string $binary, ?int $length = null): self + public static function fromBinary(string $binary, int &$offset): array { - return new static(null); + $attrName = self::nameFromBinary($binary, $offset); + $offset += 2; // Value length + + return [$attrName, new static(null)]; } } diff --git a/src/Api/Cups/Types/RangeOfInteger.php b/src/Api/Cups/Types/RangeOfInteger.php index 873051d..27c2298 100644 --- a/src/Api/Cups/Types/RangeOfInteger.php +++ b/src/Api/Cups/Types/RangeOfInteger.php @@ -24,10 +24,17 @@ public function encode(): string return pack('n', 8) . pack('N', $this->value[0]) . pack('N', $this->value[1]); } - public static function fromBinary(string $binary, ?int $length = null): self + public static function fromBinary(string $binary, int &$offset): array { - $value = unpack('Nl/Nu', $binary); - return new static([$value['l'], $value['u']]); + $attrName = self::nameFromBinary($binary, $offset); + + $valueLen = (unpack('n', $binary, $offset))[1]; + $offset += 2; + + $value = unpack('Nl/Nu', $binary, $offset); + $offset += $valueLen; + + return [$attrName, new static([$value['l'], $value['u']])]; } /** diff --git a/src/Api/Cups/Types/Resolution.php b/src/Api/Cups/Types/Resolution.php index fe71fb3..8643f69 100644 --- a/src/Api/Cups/Types/Resolution.php +++ b/src/Api/Cups/Types/Resolution.php @@ -26,10 +26,16 @@ public function encode(): string . pack('c', $reverseMap[$matches[3]]); } - - public static function fromBinary(string $binary, ?int $length = null): self + public static function fromBinary(string $binary, int &$offset): array { - $value = unpack('Np/Np2/cu', $binary); - return new static($value['p'] . 'x' . $value['p2'] . static::$unitMap[$value['u']]); + $attrName = self::nameFromBinary($binary, $offset); + + $valueLen = (unpack('n', $binary, $offset))[1]; + $offset += 2; + + $value = unpack('Np/Np2/cu', $binary, $offset); + $offset += $valueLen; + + return [$attrName, new static($value['p'] . 'x' . $value['p2'] . static::$unitMap[$value['u']])]; } } diff --git a/src/Api/Cups/Version.php b/src/Api/Cups/Version.php index 7b69489..cf1d8b7 100644 --- a/src/Api/Cups/Version.php +++ b/src/Api/Cups/Version.php @@ -6,6 +6,8 @@ enum Version: string { case V1_0 = '1.0'; case V1_1 = '1.1'; + case V2_0 = '2.0'; + case V2_1 = '2.1'; public function encode(): string { diff --git a/src/Drivers/Cups/Cups.php b/src/Drivers/Cups/Cups.php index e765525..8455f9c 100644 --- a/src/Drivers/Cups/Cups.php +++ b/src/Drivers/Cups/Cups.php @@ -33,7 +33,7 @@ public function newPrintTask(): \Rawilk\Printing\Contracts\PrintTask public function printer($printerId = null): ?Printer { $request = new Request(); - $request->setVersion(Version::V1_1) + $request->setVersion(Version::V2_1) ->setOperation(Operation::GET_PRINTER_ATTRIBUTES) ->addOperationAttributes(['printer-uri' => new Uri($printerId)]); @@ -51,7 +51,7 @@ public function printer($printerId = null): ?Printer public function printers(?int $limit = null, ?int $offset = null, ?string $dir = null): \Illuminate\Support\Collection { $request = new Request(); - $request->setVersion(Version::V1_1) + $request->setVersion(Version::V2_1) ->setOperation(Operation::CUPS_GET_PRINTERS); $printers = $this->api->makeRequest($request)->getPrinters(); @@ -62,7 +62,7 @@ public function printers(?int $limit = null, ?int $offset = null, ?string $dir = public function printJob($jobId = null): ?PrintJob { $request = new Request(); - $request->setVersion(Version::V1_1) + $request->setVersion(Version::V2_1) ->setOperation(Operation::GET_JOB_ATTRIBUTES) ->addOperationAttributes([ 'job-uri' => new Uri($jobId), @@ -87,7 +87,7 @@ public function printJob($jobId = null): ?PrintJob public function printerPrintJobs($printerId, ?int $limit = null, ?int $offset = null, ?string $dir = null): \Illuminate\Support\Collection { $request = new Request(); - $request->setVersion(Version::V1_1) + $request->setVersion(Version::V2_1) ->setOperation(Operation::GET_JOBS) ->addOperationAttributes([ 'printer-uri' => new Uri($printerId), diff --git a/src/Drivers/Cups/PrintTask.php b/src/Drivers/Cups/PrintTask.php index b03169f..4b913c0 100644 --- a/src/Drivers/Cups/PrintTask.php +++ b/src/Drivers/Cups/PrintTask.php @@ -103,15 +103,6 @@ public function sides(string $value): self return $this; } - /** - * @param string $tray - */ - public function tray($value): self - { - $this->option('media', new Keyword($value)); - return $this; - } - public function send(): PrintJob { $this->ensureValidJob(); From 468731a4db6d6f0be40da042765fa914042a49f6 Mon Sep 17 00:00:00 2001 From: vatsake Date: Mon, 6 May 2024 09:41:04 +0300 Subject: [PATCH 09/10] user --- src/Drivers/Cups/PrintTask.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Drivers/Cups/PrintTask.php b/src/Drivers/Cups/PrintTask.php index 4b913c0..1cd1a0a 100644 --- a/src/Drivers/Cups/PrintTask.php +++ b/src/Drivers/Cups/PrintTask.php @@ -81,6 +81,12 @@ public function copies(int $copies): self return $this; } + public function user(string $name): self + { + $this->option('requesting-user-name', new NameWithoutLanguage($name)); + return $this; + } + public function range($start, $end = null): self { if (!array_key_exists('page-ranges', $this->options)) { From 20b293647e500e9fd8614cfb77f847bca4526961 Mon Sep 17 00:00:00 2001 From: vatsake Date: Mon, 6 May 2024 09:50:10 +0300 Subject: [PATCH 10/10] ascii characters only --- src/Drivers/Cups/PrintTask.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Drivers/Cups/PrintTask.php b/src/Drivers/Cups/PrintTask.php index 1cd1a0a..48718f3 100644 --- a/src/Drivers/Cups/PrintTask.php +++ b/src/Drivers/Cups/PrintTask.php @@ -83,7 +83,7 @@ public function copies(int $copies): self public function user(string $name): self { - $this->option('requesting-user-name', new NameWithoutLanguage($name)); + $this->option('requesting-user-name', new NameWithoutLanguage(iconv("UTF-8", "ASCII//TRANSLIT", $name))); return $this; }