From 13b64ced2e88a14723d975522f437d5133c237ce Mon Sep 17 00:00:00 2001 From: Anton Thomasson Date: Tue, 30 May 2023 19:18:14 +0200 Subject: [PATCH] WIP: ipp --- Makefile | 6 +- lib/ippattr.cpp | 128 +++++++ lib/ippattr.h | 147 ++++++++ lib/ippmsg.cpp | 435 ++++++++++++++++++++++++ lib/ippmsg.h | 107 ++++++ lib/ippparameters.cpp | 337 ++++++++++++++++++ lib/ippparameters.h | 298 ++++++++++++++++ lib/list.h | 32 ++ lib/polymorph.h | 28 ++ lib/printparameters.h | 4 +- tests/Makefile | 2 +- tests/test.cpp | 638 +++++++++++++++++++++++++++++++++++ utils/ippposter.cpp | 420 ++++++++++++++++++++++- utils/pdf2printable_main.cpp | 10 +- 14 files changed, 2568 insertions(+), 24 deletions(-) create mode 100644 lib/ippattr.cpp create mode 100644 lib/ippattr.h create mode 100644 lib/ippmsg.cpp create mode 100644 lib/ippmsg.h create mode 100644 lib/ippparameters.cpp create mode 100644 lib/ippparameters.h create mode 100644 lib/list.h create mode 100644 lib/polymorph.h diff --git a/Makefile b/Makefile index 3c72152..8ec4505 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -CXXFLAGS = -std=c++17 -O3 -pedantic -Wall -Wextra -Werror -Ilib -Ibytestream \ +CXXFLAGS = -std=c++17 -g -pedantic -Wall -Wextra -Werror -Ilib -Ibytestream \ $(shell pkg-config --cflags poppler-glib) VPATH = bytestream lib utils @@ -36,8 +36,8 @@ baselinify: bytestream.o baselinify.o baselinify_main.o baselinify_mad: bytestream.o baselinify_mad.o baselinify_main.o $(CXX) $^ -ldl -o $@ -ippposter: ippposter.o curlrequester.o bytestream.o - $(CXX) $^ -lcurl -o $@ +ippposter: ippmsg.o ippattr.o ippparameters.o printparameters.o ippposter.o curlrequester.o pdf2printable.o ppm2pwg.o baselinify.o bytestream.o + $(CXX) $^ $(shell pkg-config --libs poppler-glib) -ljpeg -lcurl -o $@ clean: rm -f *.o ppm2pwg pwg2ppm pdf2printable pdf2printable_mad hexdump baselinify baselinify_mad diff --git a/lib/ippattr.cpp b/lib/ippattr.cpp new file mode 100644 index 0000000..5fd7717 --- /dev/null +++ b/lib/ippattr.cpp @@ -0,0 +1,128 @@ +#include "ippattr.h" +#include + +bool IppAttr::isList() const +{ + return is(); +} +IppOneSetOf IppAttr::asList() const +{ + if(isList()) + { + return get(); + } + else + { + return IppOneSetOf {{value()}}; + } +} + +std::ostream& operator<<(std::ostream& os, const IppIntRange& ir) +{ + os << "{\"min\": " << ir.low << ", \"max\": " << ir.high << "}"; + return os; +} +std::ostream& operator<<(std::ostream& os, const IppResolution& res) +{ + os << "{\"x\": " << res.x + << ", \"y\": " << res.y + << ", \"units\": " << +res.units << "}"; + return os; +} +std::ostream& operator<<(std::ostream& os, const IppDateTime& dt) +{ + // 2000-01-02T00:00:57 GMT+0200 + os << "\"" << std::setfill('0') << std::setw(4) << dt.year << "-" + << std::setw(2) << +dt.month << "-" << std::setw(2) << +dt.day + << "T" << std::setw(2) << +dt.hour << ":" + << std::setw(2) << +dt.minutes << ":" << std::setw(2) << +dt.seconds + << "." << std::setw(3) << dt.deciSeconds*100 << " GMT" << dt.plusMinus + << std::setw(2) << +dt.utcHOffset << std::setw(2) << +dt.utcMOffset << "\""; + return os; +} + +std::ostream& operator<<(std::ostream& os, const IppValue& iv) +{ + if(iv.is()) + { + os << "\"" << iv.get() << "\""; + } + else if(iv.is()) + { + os << iv.get(); + } + else if(iv.is()) + { + os << iv.get(); + } + else if(iv.is()) + { + os << iv.get(); + } + else if(iv.is()) + { + os << iv.get(); + } + else if(iv.is()) + { + os << iv.get(); + } + else if(iv.is()) + { + IppOneSetOf oneSet = iv.get(); + os << "[" << oneSet.takeFront(); + for(IppValue iv2 : oneSet) + { + os << ", " << iv2; + } + os << "]"; + } + else if(iv.is()) + { + os << "{"; + IppCollection ippCollection = iv.get(); + IppCollection::const_iterator it = ippCollection.cbegin(); + os << "\"" << it->first << "\": " << it->second; + it++; + for(; it != ippCollection.cend(); it++) + { + os << ", \"" << it->first << "\": " << it->second; + } + os << "}"; + } + else + { + os << "unhandled value"; + } + + return os; +} + + +std::ostream& operator<<(std::ostream& os, const IppAttrs& as) +{ + if(as.empty()) + { + os << "{}" << std::endl; + return os; + } + + IppAttrs::const_iterator it = as.cbegin(); + os << "{" + << "\"" << it->first << "\": {\"tag\": " << +it->second.tag() + << ", \"value\": " << it->second.value() << "}"; + it++; + for(; it != as.cend(); it++) + { + os << "," << std::endl + << "\"" << it->first << "\": {\"tag\": " << +it->second.tag() + << ", \"value\": " << it->second.value() << "}"; + } + os << "}" << std::endl; + return os; +} + +bool IppCollection::has(std::string key) const +{ + return find(key) != end(); +} diff --git a/lib/ippattr.h b/lib/ippattr.h new file mode 100644 index 0000000..084af02 --- /dev/null +++ b/lib/ippattr.h @@ -0,0 +1,147 @@ +#ifndef IPPATTR_H +#define IPPATTR_H + +#include "list.h" +#include +#include +#include +#include +#include +#include "polymorph.h" + +struct IppOneSetOf; +struct IppCollection; +class IppAttr; + +struct IppIntRange +{ + int32_t low = 0; + int32_t high = 0; + + bool operator==(const IppIntRange& other) const + { + return other.low == low && other.high == high; + } +}; + +struct IppResolution +{ + enum Units : uint8_t + { + Invalid=0, + DPI=3, + DPCM=4 + }; + + uint32_t x = 0; + uint32_t y = 0; + uint8_t units = Invalid; + + bool operator==(const IppResolution& other) const + { + return other.units == units && other.x == x && other.y == y; + } +}; + +struct IppDateTime +{ + uint16_t year = 0; + uint8_t month = 0; + uint8_t day = 0; + uint8_t hour = 0; + uint8_t minutes = 0; + uint8_t seconds = 0; + uint8_t deciSeconds = 0; + uint8_t plusMinus = '+'; + uint8_t utcHOffset = 0; + uint8_t utcMOffset = 0; + + bool operator==(const IppDateTime& other) const + { + return other.year == year && + other.month == month && + other.day == day && + other.hour == hour && + other.minutes == minutes && + other.seconds == seconds && + other.deciSeconds == deciSeconds && + other.plusMinus == plusMinus && + other.utcHOffset == utcHOffset && + other.utcMOffset == utcMOffset; + } +}; + +using IppValue = Polymorph; + +struct IppOneSetOf: public List +{ + using List::List; +}; + +struct IppCollection: public std::map +{ + using std::map::map; + bool has(std::string key) const; +}; + +class IppAttr : public IppValue +{ +public: + bool isList() const ; + IppOneSetOf asList() const; + + template + IppAttr(uint8_t tag, T value) : IppValue(value), _tag(tag) {} + + uint8_t tag() const {return _tag;} + IppValue value() const {return *this;} + +private: + uint8_t _tag; + +}; + +struct IppAttrs: public std::map +{ + using std::map::map; + + bool has(std::string key) const + { + return find(key) != end(); + } + + template + T get(std::string name, T res=T()) const + { + if(find(name) != end()) + { + res = at(name).get(); + } + return res; + } + + template + List getList(std::string name) const + { + List res; + if(has(name)) + { + for(const IppValue& v : at(name).asList()) + { + res.push_back(v.get()); + } + } + return res; + } +}; + +std::ostream& operator<<(std::ostream& os, const IppIntRange& iv); +std::ostream& operator<<(std::ostream& os, const IppResolution& iv); +std::ostream& operator<<(std::ostream& os, const IppDateTime& iv); +std::ostream& operator<<(std::ostream& os, const IppValue& iv); + +std::ostream& operator<<(std::ostream& os, const IppAttrs& as); + +#endif // IPPATTR_H diff --git a/lib/ippmsg.cpp b/lib/ippmsg.cpp new file mode 100644 index 0000000..61ed351 --- /dev/null +++ b/lib/ippmsg.cpp @@ -0,0 +1,435 @@ +#include "ippmsg.h" +#include + +inline bool startsWith(std::string s, std::string start) +{ + if(start.length() < s.length()) + { + return s.substr(0, start.length()) == start; + } + return false; +} + +uint32_t IppMsg::_reqId=1; + +IppMsg::IppMsg(Bytestream& bts) +{ + uint32_t reqId; + + bts >> _majVsn >> _minVsn >> _opOrStatus >> reqId; + + IppAttrs attrs; + Tag currentAttrType = EndAttrs; + + while(!bts.atEnd()) + { + if(bts.peekU8() <= UnsupportedAttrs) + { + if(currentAttrType == OpAttrs) + { + _opAttrs = attrs; + } + else if (currentAttrType == JobAttrs) + { + _jobAttrs.push_back(attrs); + } + else if (currentAttrType == PrinterAttrs) + { + _printerAttrs = attrs; + } + else if (currentAttrType == UnsupportedAttrs) + { + std::cerr << "WARNING: unsupported attrs reported: " << std::endl + << attrs; + } + + if(bts >>= EndAttrs) + { + break; + } + + currentAttrType = (Tag)bts.getU8(); + attrs = IppAttrs(); + } + else + { + consumeAttributes(attrs, bts); + } + } +} + +IppMsg::IppMsg(uint16_t opOrStatus, IppAttrs opAttrs, IppAttrs jobAttrs, + uint8_t majVsn, uint8_t minVsn, IppAttrs printerAttrs) +{ + _opOrStatus = opOrStatus; + _majVsn = majVsn; + _minVsn = minVsn; + _opAttrs = opAttrs; + _jobAttrs = {jobAttrs}; + _printerAttrs = printerAttrs; +} + +Bytestream IppMsg::encode() const +{ + Bytestream ipp; + ipp << _majVsn << _minVsn; + ipp << _opOrStatus; + ipp << _reqId++; + ipp << OpAttrs; + + IppAttrs opAttrs = _opAttrs; + + // attributes-charset and attributes-natural-language are required to be first + // some printers fail if the other mandatory parameters are not in this specific order + std::list InitialAttrs = {"attributes-charset", + "attributes-natural-language", + "printer-uri", + "job-id", + "requesting-user-name"}; + for(std::string key : InitialAttrs) + { + if(opAttrs.has(key)) + { + IppAttr val = opAttrs.at(key); + encodeAttribute(ipp, key, val); + opAttrs.erase(key); + } + } + encodeAttributes(ipp, opAttrs); + for(IppAttrs attrs : _jobAttrs) + { + if (!attrs.empty()) { + ipp << JobAttrs; + encodeAttributes(ipp, attrs); + } + } + if (!_printerAttrs.empty()) { + ipp << PrinterAttrs; + encodeAttributes(ipp, _printerAttrs); + } + ipp << EndAttrs; + return ipp; +} + +void IppMsg::setOpAttr(std::string name, IppAttr attr) +{ + _opAttrs.insert({name, attr}); +} + + +IppValue IppMsg::decodeValue(uint8_t tag, Bytestream& data) const +{ + uint16_t tmpLen; + + switch (tag) + { + case OpAttrs: + case JobAttrs: + case EndAttrs: + case PrinterAttrs: + case UnsupportedAttrs: + throw std::logic_error("Unexpected tag"); + case Integer: + case Enum: + { + uint32_t tmp_u32=0; + if(!(data >>= (uint16_t)0)) + { + data >> (uint16_t)4 >> tmp_u32; + } + return IppValue((int)tmp_u32); + } + case Boolean: + { + uint8_t tmp_bool=0; + if(!(data >>= (uint16_t)0)) + { + data >> (uint16_t)1 >> tmp_bool; + } + return IppValue((bool)tmp_bool); + } + case DateTime: + { + IppDateTime tmpDateTime; + if(!(data >>= (uint16_t)0)) + { + data >> (uint16_t)11 >> tmpDateTime.year >> tmpDateTime.month >> tmpDateTime.day + >> tmpDateTime.hour >> tmpDateTime.minutes >> tmpDateTime.seconds >> tmpDateTime.deciSeconds + >> tmpDateTime.plusMinus >> tmpDateTime.utcHOffset >> tmpDateTime.utcMOffset; + } + return IppValue(tmpDateTime); + } + case Resolution: + { + IppResolution tmpResolution; + if(!(data >>= (uint16_t)0)) + { + data >> (uint16_t)9 >> tmpResolution.x >> tmpResolution.y >> tmpResolution.units; + } + return IppValue(tmpResolution); + } + case IntegerRange: + { + IppIntRange tmpIntegerRange; + if(!(data >>= (uint16_t)0)) + { + data >> (uint16_t)8 >> tmpIntegerRange.low >> tmpIntegerRange.high; + } + return IppValue(tmpIntegerRange); + } + case OctetStringUnknown: + case TextWithLanguage: + case NameWithLanguage: + case TextWithoutLanguage: + case NameWithoutLanguage: + case Keyword: + case Uri: + case UriScheme: + case Charset: + case NaturalLanguage: + case MimeMediaType: + default: + { + std::string tmp_str = ""; + data >> tmpLen; + data/tmpLen >> tmp_str; + return IppValue(tmp_str); + } + }; +} + +List IppMsg::getUnnamedAttributes(Bytestream& data) const +{ + uint8_t tag; + List attrs; + while(data.remaining()) + { + data >> tag; + if(data >>= (uint16_t)0) + { + attrs.push_back(IppAttr(tag, decodeValue(tag, data))); + } + else + { + data -= 1; + break; + } + } + return attrs; +} + +IppValue IppMsg::collectAttributes(List& attrs) const +{ + IppOneSetOf resArr; + IppCollection resVal; + while(!attrs.empty()) + { + IppAttr tmpval = attrs.takeFront(); + uint8_t tag = tmpval.tag(); + if(tag == MemberName) + { + std::string key = tmpval.get(); + tmpval = attrs.takeFront(); + if(tmpval.tag() == BeginCollection) + { + resVal.insert({key, IppAttr(BeginCollection, collectAttributes(attrs))}); + } + else + { // This should be general data attributes + IppOneSetOf restOfSet; + while(attrs.front().tag() == tmpval.tag()) + { + restOfSet.push_back(attrs.takeFront().value()); + } + + if(restOfSet.empty()) + { + resVal.insert({key, tmpval}); + } + else + { + restOfSet.push_front(tmpval.value()); + tmpval = IppAttr(tmpval.tag(), restOfSet); + resVal.insert({key, tmpval}); + } + } + } + else if(tag == EndCollection) + { + resArr.push_back(resVal); + resVal = IppCollection(); + if(attrs.empty()) + { // end of collection + break; + } + else if(attrs.front().tag() == BeginCollection) + { // this is a 1setOf + attrs.pop_front(); + continue; + } + else + { // the following attribute(s) belong to an outer collection + break; + } + } + else + { + std::cerr << "out of sync with collection" << tmpval.tag(); + } + } + + if(resArr.size()==1) + { // The code above unconditionally produces arrays, collapse if they are just a single object + return resArr.front(); + } + else + { + return resArr; + } +} + +std::string IppMsg::consumeAttributes(IppAttrs& attrs, Bytestream& data) const +{ + uint8_t tag; + uint16_t tmpLen; + std::string name; + + data >> tag >> tmpLen; + data/tmpLen >> name; + + IppValue value = decodeValue(tag, data); + List unnamed = getUnnamedAttributes(data); + + if(tag == BeginCollection) + { + value = collectAttributes(unnamed); + } + + if(!unnamed.empty()) + { + IppOneSetOf tmpOneSet; + tmpOneSet.push_back(value); + for(IppAttr a : unnamed) + { + tmpOneSet.push_back(a.value()); + } + attrs.insert({name, IppAttr(tag, tmpOneSet)}); + } + else + { + attrs.insert({name, IppAttr(tag, value)}); + } + return name; +} + +void IppMsg::encodeAttributes(Bytestream& msg, const IppAttrs& attrs) const +{ + for(std::pair attr : attrs) + { + encodeAttribute(msg, attr.first, attr.second); + } +} + +void IppMsg::encodeAttribute(Bytestream& msg, std::string name, IppAttr attr, bool subCollection) const +{ + uint8_t tag = attr.tag(); + if(subCollection) + { + msg << MemberName << (uint16_t)0 << (uint16_t)name.length() << name; + name = ""; + } + + msg << tag << uint16_t(name.length()) << name; + + if(attr.value().is()) + { + IppOneSetOf oneSet = attr.get(); + encodeValue(msg, tag, oneSet.takeFront()); + while(!oneSet.empty()) + { + msg << tag << uint16_t(0); + encodeValue(msg, tag, oneSet.takeFront()); + } + } + else + { + encodeValue(msg, attr.tag(), attr.value()); + } +} + +void IppMsg::encodeValue(Bytestream& msg, uint8_t tag, IppValue val) const +{ + switch(tag) + { + case OpAttrs: + case JobAttrs: + case EndAttrs: + case PrinterAttrs: + throw std::logic_error("Unexpected tag"); + case Integer: + case Enum: + { + uint32_t tmp_u32 = val.get(); + msg << (uint16_t)4 << tmp_u32; + break; + } + case Boolean: + { + uint8_t tmp_u8 = val.get(); + msg << (uint16_t)1 << tmp_u8; + break; + } + case DateTime: + { + IppDateTime tmpDateTime = val.get(); + msg << (uint16_t)11 << tmpDateTime.year << tmpDateTime.month << tmpDateTime.day + << tmpDateTime.hour << tmpDateTime.minutes << tmpDateTime.seconds << tmpDateTime.deciSeconds + << tmpDateTime.plusMinus << tmpDateTime.utcHOffset << tmpDateTime.utcMOffset; + break; + } + case Resolution: + { + IppResolution tmpRes = val.get(); + msg << (uint16_t)9 << tmpRes.x << tmpRes.y << tmpRes.units; + break; + } + case IntegerRange: + { + IppIntRange tmpRange = val.get(); + msg << (uint16_t)8 << tmpRange.low << tmpRange.high; + break; + } + case BeginCollection: + { + msg << (uint16_t)0; // length of value + IppCollection collection = val.get(); + for(auto const& x : collection) + { + encodeAttribute(msg, x.first, x.second, true); + } + msg << EndCollection << (uint16_t)0 << (uint16_t)0; + break; + } + case OctetStringUnknown: + case TextWithLanguage: + case NameWithLanguage: + case TextWithoutLanguage: + case NameWithoutLanguage: + case Keyword: + case Uri: + case UriScheme: + case Charset: + case NaturalLanguage: + case MimeMediaType: + { + std::string tmpstr = val.get(); + msg << uint16_t(tmpstr.length()); + msg.putBytes(tmpstr.data(), tmpstr.length()); + break; + } + default: + std::cerr << "uncaught tag " << tag; + throw std::logic_error("Uncaught tag"); + break; + } +} diff --git a/lib/ippmsg.h b/lib/ippmsg.h new file mode 100644 index 0000000..d648b8a --- /dev/null +++ b/lib/ippmsg.h @@ -0,0 +1,107 @@ +#ifndef IPPMSG_H +#define IPPMSG_H + +#include +#include +#include +#include +#include "ippattr.h" + +class IppMsg +{ +public: + + enum Operation : uint16_t + { + PrintJob = 0x0002, + PrintUri = 0x0003, + ValidateJob = 0x0004, + CreateJob = 0x0005, + SendDocument = 0x0006, + SendUri = 0x0007, + CancelJob = 0x0008, + GetJobAttrs = 0x0009, + GetJobs = 0x000A, + GetPrinterAttrs = 0x000B, + HoldJob = 0x000C, + ReleaseJob = 0x000D, + RestartJob = 0x000E, + PausePrinter = 0x0010, + ResumePrinter = 0x0011, + PurgeJobs = 0x0012, + IdentifyPrinter = 0x003C + }; + + enum Tag : uint8_t + { + OpAttrs = 0x01, + JobAttrs = 0x02, + EndAttrs = 0x03, + PrinterAttrs = 0x04, + UnsupportedAttrs = 0x05, + Unsupported = 0x10, + Integer = 0x21, + Boolean = 0x22, + Enum = 0x23, + OctetStringUnknown = 0x30, + DateTime = 0x31, + Resolution = 0x32, + IntegerRange = 0x33, + BeginCollection = 0x34, + TextWithLanguage = 0x35, + NameWithLanguage = 0x36, + EndCollection = 0x37, + TextWithoutLanguage = 0x41, + NameWithoutLanguage = 0x42, + Keyword = 0x44, + Uri = 0x45, + UriScheme = 0x46, + Charset = 0x47, + NaturalLanguage = 0x48, + MimeMediaType = 0x49, + MemberName = 0x4A + }; + + IppMsg() = delete; + IppMsg(Bytestream& msg); + IppMsg(uint16_t opOrStatus, IppAttrs opAttrs, IppAttrs jobAttrs=IppAttrs(), + uint8_t majVsn=1, uint8_t minVsn=1, IppAttrs printerAttrs=IppAttrs()); + IppMsg(const IppMsg& other) = default; + ~IppMsg() = default; + + IppAttrs getPrinterAttrs() const {return _printerAttrs;} + std::list getJobAttrs() const {return _jobAttrs;} + IppAttrs getOpAttrs() const {return _opAttrs;} + uint16_t getStatus() const {return _opOrStatus;} + + Bytestream encode() const; + void setOpAttr(std::string name, IppAttr attr); + + static void setReqId(uint32_t reqId) + { + _reqId = reqId; + } + +private: + IppValue decodeValue(uint8_t tag, Bytestream& data) const; + List getUnnamedAttributes(Bytestream& data) const; + IppValue collectAttributes(List& attrs) const; + std::string consumeAttributes(IppAttrs& attrs, Bytestream& data) const; + void encodeAttributes(Bytestream& msg, const IppAttrs& attrs) const; + void encodeAttribute(Bytestream& msg, std::string name, IppAttr attr, bool subCollection=false) const; + void encodeValue(Bytestream& msg, uint8_t tag, IppValue val) const; + + uint16_t _opOrStatus; + + uint8_t _majVsn; + uint8_t _minVsn; + + IppAttrs _opAttrs; + std::list _jobAttrs; + IppAttrs _printerAttrs; + + static uint32_t _reqId; + +}; + +#endif // IPPMSG_H diff --git a/lib/ippparameters.cpp b/lib/ippparameters.cpp new file mode 100644 index 0000000..5ad3197 --- /dev/null +++ b/lib/ippparameters.cpp @@ -0,0 +1,337 @@ +#include "ippparameters.h" +#include "mediaposition.h" + +inline bool startsWith(std::string s, std::string start) +{ + if(start.length() <= s.length()) + { + return s.substr(0, start.length()) == start; + } + return false; +} + +// TODO: add a callback for unhandled formats? demand they are handled first? +IppParameters::Error IppParameters::finalize(std::string inputFormat, int pages) +{ + // maybe find mime type? + + // handle additional formats + + std::string targetFormat = determineTargetFormat(inputFormat); + // TODO: only set if regular supported format - else set OctetSteam + documentFormat.set(targetFormat); + + if(targetFormat == "" || targetFormat == OctetStream) + { + return Error("Failed to determine traget format"); + } + + if(!printParams.setPaperSize(media.get(printParams.paperSizeName))) + { + return Error("Invalid paper size name"); + } + + // qDebug() << "target format:" << targetFormat; + + // Only keep margin setting for image formats + if(!isImage(targetFormat)) + { + topMargin.unset(); + bottomMargin.unset(); + leftMargin.unset(); + rightMargin.unset(); + } + + if(jobAttrs.has("media-col") && media.isSet()) + { + int x = printParams.getPaperSizeWInMillimeters()*100; + int y = printParams.getPaperSizeHInMillimeters()*100; + + // TODO: add settings for these? Private? + IppCollection dimensions {{"x-dimension", IppAttr(IppMsg::Integer, x)}, + {"y-dimension", IppAttr(IppMsg::Integer, y)}}; + + IppCollection mediaCol = jobAttrs.get("media-col"); + mediaCol.insert_or_assign("media-size", IppAttr(IppMsg::BeginCollection, dimensions)); + jobAttrs.insert_or_assign("media-col", IppAttr(IppMsg::BeginCollection, mediaCol)); + media.unset(); + } + + // TODO: make a setter? + if(inputFormat == PDF) + { + if(targetFormat == PDF) + { + printParams.format = PrintParameters::PDF; + } + else if(targetFormat == Postscript) + { + printParams.format = PrintParameters::Postscript; + } + else if(targetFormat == PWG) + { + printParams.format = PrintParameters::PWG; + } + else if(targetFormat == URF) + { + printParams.format = PrintParameters::URF; + } + else + { + printParams.format = PrintParameters::Invalid; + } + } + else + { + printParams.format = PrintParameters::Invalid; + } + + // qDebug() << "Printing job" << jobOpAttrs << jobAttrs; + + IppResolution ippResolution = resolution.get({printParams.hwResW, printParams.hwResH, IppResolution::DPI}); + printParams.hwResW = ippResolution.x; + printParams.hwResH = ippResolution.y; + + // Effected locally for PDF (and upstream formats) + if(inputFormat == PDF) + { + for(IppIntRange range : jobAttrs.getList("page-ranges")) + { + printParams.pageRangeList.push_back({range.low, range.high}); + } + pageRanges.unset(); + } + + adjustRasterSettings(pages); + + if(sides.get() == "two-sided-long-edge") + { + printParams.duplexMode = PrintParameters::TwoSidedLongEdge; + } + else if(sides.get() == "two-sided-short-edge") + { + printParams.duplexMode = PrintParameters::TwoSidedShortEdge; + } + + switch (printQuality.get()) + { + case 3: + printParams.quality = PrintParameters::DraftQuality; + break; + case 4: + printParams.quality = PrintParameters::NormalQuality; + break; + case 5: + printParams.quality = PrintParameters::HighQuality; + break; + default: + printParams.quality = PrintParameters::DefaultQuality; + break; + } + + bool supportsColor = colorMode.getSupported().contains("color"); + + printParams.colorMode = colorMode.get().find("color") != std::string::npos ? PrintParameters::sRGB24 + : colorMode.get().find("monochrome") != std::string::npos + || !supportsColor ? PrintParameters::Gray8 + : printParams.colorMode; + + std::map MediaPositionMap MEDIA_POSITION_MAP; + if(mediaSource.isSet()) + { + printParams.mediaPosition = MediaPositionMap.at(mediaSource.get()); + } + printParams.mediaType = mediaType.get(); + + return Error(); +} + +std::string firstMatch(List supported, List wanted) +{ + for(const std::string& w : wanted) + { + if(supported.contains(w)) + { + return w; + } + } + return ""; +} + +std::string IppParameters::determineTargetFormat(std::string inputFormat) +{ + std::string targetFormat = documentFormat.get(OctetStream); + List pdfPrioList = {PDF, Postscript, PWG, URF}; + + bool imageToImage = isImage(inputFormat) && isImage(targetFormat); + if(!documentFormat.isSet() && !(pdfPrioList.contains(targetFormat) || imageToImage)) + { // User made no choice, and we don't know the target format - treat as if auto + targetFormat = OctetStream; + } + + if(targetFormat == OctetStream) + { + if(inputFormat == PDF) + { // PDFs we can convert, pick the best supported format + return firstMatch(documentFormat.getSupported(), pdfPrioList); + } + else if(documentFormat.getSupported().contains(inputFormat)) + { // Natively supported format + return inputFormat; + } + return targetFormat; + } + return targetFormat; +} + +bool IppParameters::isImage(std::string format) +{ + return startsWith(format, "image/") && format != URF && format != PWG; +} + +void IppParameters::adjustRasterSettings(int pages) +{ + if(printParams.format != PrintParameters::PWG && printParams.format != PrintParameters::URF) + { + return; + } + + resolution.unset(); + + if(printParams.format == PrintParameters::PWG) + { + uint32_t diff = std::numeric_limits::max(); + uint32_t AdjustedHwResX = printParams.hwResW; + uint32_t AdjustedHwResY = printParams.hwResH; + for(IppResolution res : _printerAttrs.getList("pwg-raster-document-resolution-supported")) + { + if(res.units != IppResolution::DPI) + { + continue; + } + uint32_t tmpDiff = std::abs(int(printParams.hwResW-res.x)) + std::abs(int(printParams.hwResH-res.y)); + if(tmpDiff < diff) + { + diff = tmpDiff; + AdjustedHwResX = res.x; + AdjustedHwResY = res.y; + } + } + printParams.hwResW = AdjustedHwResX; + printParams.hwResH = AdjustedHwResY; + } + else if(printParams.format == PrintParameters::URF) + { // Ensure symmetric resolution for URF + printParams.hwResW = printParams.hwResH = std::min(printParams.hwResW, printParams.hwResH); + + uint32_t diff = std::numeric_limits::max(); + uint32_t AdjustedHwRes = printParams.hwResW; + + List urfSupported = _printerAttrs.getList("urf-supported"); + for(std::string us : urfSupported) + { + if(startsWith(us, "RS")) + { //RS300[-600] + us = us.substr(2); + size_t pos = 0; + while(pos <= us.length()) + { + size_t found = std::min(us.length(), us.find("-", pos)); + std::string substr(us, pos, (found-pos)); + pos = found+1; + + int intRes = std::stoi(substr); + uint32_t tmpDiff = std::abs(int(printParams.hwResW - intRes)); + if(tmpDiff < diff) + { + diff = tmpDiff; + AdjustedHwRes = intRes; + } + } + printParams.hwResW = printParams.hwResH = AdjustedHwRes; + break; + } + } + } + + if(printParams.format == PrintParameters::PWG) + { + std::string DocumentSheetBack = _printerAttrs.get("pwg-raster-document-sheet-back"); + if(DocumentSheetBack=="flipped") + { + printParams.backXformMode=PrintParameters::Flipped; + } + else if(DocumentSheetBack=="rotated") + { + printParams.backXformMode=PrintParameters::Rotated; + } + else if(DocumentSheetBack=="manual-tumble") + { + printParams.backXformMode=PrintParameters::ManualTumble; + } + } + else if(printParams.format == PrintParameters::URF) + { + List urfSupported = _printerAttrs.getList("urf-supported"); + if(urfSupported.contains("DM2")) + { + printParams.backXformMode=PrintParameters::Flipped; + } + else if(urfSupported.contains("DM3")) + { + printParams.backXformMode=PrintParameters::Rotated; + } + else if(urfSupported.contains("DM4")) + { + printParams.backXformMode=PrintParameters::ManualTumble; + } + } + + int copiesRequested = copies.get(1); + // Actual (non-support) value can be 1, non-presense will be 0 + bool supportsCopies = _printerAttrs.get("copies-supported").high > 1; + + List varyingAttributes = _printerAttrs.getList("document-format-varying-attributes"); + if(varyingAttributes.contains("copies") || varyingAttributes.contains("copies-supported")) + { + supportsCopies = false; + } + + if(copiesRequested > 1 && !supportsCopies) + { + std::string copyMode = multipleDocumentHandling.get(); + // qDebug() << "Doing local copies" << copyMode << copiesRequested; + printParams.copies = copiesRequested; + if(copyMode == "separate-documents-uncollated-copies") + { // Only do silly copies if explicitly requested + printParams.collatedCopies = false; + } + copies.unset(); + + if(sides.get() != "one-sided") + { + bool singlePageRange = false; + if(printParams.pageRangeList.size() == 1) + { + size_t fromPage = printParams.pageRangeList.begin()->first; + size_t toPage = printParams.pageRangeList.begin()->second; + singlePageRange = fromPage != 0 && fromPage == toPage; + } + if(pages == 1 || singlePageRange) + { + sides.set("one-sided"); + // qDebug() << "Optimizing one-page document to one-sided"; + } + } + } +} + + +const std::string IppParameters::OctetStream = "application/octet-stream"; + +const std::string IppParameters::PDF = "application/pdf"; +const std::string IppParameters::Postscript = "application/postscript"; +const std::string IppParameters::PWG = "image/pwg-raster"; +const std::string IppParameters::URF = "image/urf"; + +const std::string IppParameters::JPEG = "image/jpeg"; diff --git a/lib/ippparameters.h b/lib/ippparameters.h new file mode 100644 index 0000000..7fccbf7 --- /dev/null +++ b/lib/ippparameters.h @@ -0,0 +1,298 @@ +#ifndef IPPPARAMETERS_H +#define IPPPARAMETERS_H + +#include "printparameters.h" +#include "pdf2printable.h" // TODO: put WriteFun somewhere more central +#include +#include "ippmsg.h" +#include "binfile.h" +#include +#include +#include +#include + +class IppParameters +{ +public: + IppParameters(IppAttrs printerAttrs) : _printerAttrs(printerAttrs) + {} + + template + class Param + { + public: + Param() = delete; + Param& operator=(const Param& other) = delete; + Param(IppAttrs* printerAttrs, IppAttrs* attrs, IppMsg::Tag tag, std::string name, std::string subKey = "") + : _printerAttrs(printerAttrs), _attrs(attrs), _tag(tag), _name(name), _subKey(subKey) + {} + virtual bool isSupported() + { + return _printerAttrs->has(_name+"-supported"); + } + T getDefault(T fallback=T()) const + { + return _printerAttrs->get(_name+"-default", fallback); + } + void set(T value) + { + if(_subKey != "") + { + IppCollection col; + if(_attrs->has(_subKey)) + { + col = _attrs->at(_subKey).get(); + } + col.insert_or_assign(_name, IppAttr(_tag, value)); + _attrs->insert_or_assign(_subKey, IppAttr(IppMsg::BeginCollection, col)); + } + else + { + _attrs->insert_or_assign(_name, IppAttr(_tag, value)); + } + } + bool isSet() const + { + if(_subKey != "") + { + return _attrs->has(_subKey) && _attrs->at(_subKey).get().has(_name); + } + else + { + return _attrs->has(_name); + } + } + void unset() + { + if(_subKey != "") + { + IppCollection col; + if(_attrs->has(_subKey)) + { + col = _attrs->at(_subKey).get(); + } + col.erase(_name); + if(col.empty()) + { + _attrs->erase(_subKey); + } + else + { + _attrs->insert_or_assign(_subKey, IppAttr(IppMsg::BeginCollection, col)); + } + } + else + { + _attrs->erase(_name); + } + } + T get(T fallback=T()) const + { + if(isSet()) + { + if(_subKey != "") + { + return _attrs->at(_subKey).get().at(_name).get(); + } + else + { + return _attrs->at(_name).get(); + } + } + else + { + return getDefault(fallback); + } + } + protected: + IppAttrs* _printerAttrs; + IppAttrs* _attrs; + IppMsg::Tag _tag; + std::string _name; + std::string _subKey; + }; + + template + class ChoiceParam : public Param + { + public: + ChoiceParam(IppAttrs* printerAttrs, IppAttrs* attrs, IppMsg::Tag tag, std::string name, std::string subKey = "") + : Param(printerAttrs, attrs, tag, name, subKey) + {} + + List getSupported() const + { + IppAttrs* pa = this->_printerAttrs; + return pa->getList(this->_name+"-supported"); + } + }; + + template + class PreferredChoiceParam : public ChoiceParam + { + public: + PreferredChoiceParam(IppAttrs* printerAttrs, IppAttrs* attrs, IppMsg::Tag tag, std::string name, std::string pref) + : ChoiceParam(printerAttrs, attrs, tag, name), _pref(pref) + {} + + List getPreferred() const + { + IppAttrs* pa = this->_printerAttrs; + return pa->getList(this->_name+"-"+_pref); + } + + private: + std::string _pref; + }; + + class IntegerParam : public Param + { + public: + IntegerParam(IppAttrs* printerAttrs, IppAttrs* attrs, IppMsg::Tag tag, std::string name) + : Param(printerAttrs, attrs, tag, name) + {} + int getSupportedMin() + { + return _printerAttrs->get(this->_name+"-supported").low; + } + int getSupportedMax() + { + return _printerAttrs->get(this->_name+"-supported").high; + } + }; + + class IntegerRangeListParam : public Param + { + public: + IntegerRangeListParam(IppAttrs* printerAttrs, IppAttrs* attrs, IppMsg::Tag tag, std::string name) + : Param(printerAttrs, attrs, tag, name) + {} + + bool getSupported() const + { + return _printerAttrs->get(this->_name+"-supported"); + } + }; + + class IntegerChoiceParam : public Param + { + public: + IntegerChoiceParam(IppAttrs* printerAttrs, IppAttrs* attrs, IppMsg::Tag tag, std::string name) + : Param(printerAttrs, attrs, tag, name) + {} + List getSupported() const + { + List res; + if(_printerAttrs->has(_name+"-supported") && + _printerAttrs->at(_name+"-supported").is()) + { + IppIntRange range = _printerAttrs->at(_name+"-supported").get(); + for(int i = range.low; i <= range.high; i++) + { + res.push_back(i); + } + } + else + { + res = _printerAttrs->getList(_name+"-supported"); + } + return res; + } + }; + + ChoiceParam sides = ChoiceParam(&_printerAttrs, &jobAttrs, IppMsg::Keyword, "sides"); + PreferredChoiceParam media = PreferredChoiceParam(&_printerAttrs, &jobAttrs, IppMsg::Keyword, "media", "ready"); + + IntegerParam copies = IntegerParam(&_printerAttrs, &jobAttrs, IppMsg::Integer, "copies"); + ChoiceParam multipleDocumentHandling = ChoiceParam(&_printerAttrs, &jobAttrs, IppMsg::Keyword, "multiple-document-handling"); + + IntegerRangeListParam pageRanges = IntegerRangeListParam(&_printerAttrs, &jobAttrs, IppMsg::IntegerRange, "page-ranges"); + + IntegerChoiceParam numberUp = IntegerChoiceParam(&_printerAttrs, &jobAttrs, IppMsg::Integer, "number-up"); + + ChoiceParam colorMode = ChoiceParam(&_printerAttrs, &jobAttrs, IppMsg::Keyword, "print-color-mode"); + ChoiceParam printQuality = ChoiceParam(&_printerAttrs, &jobAttrs, IppMsg::Enum, "print-quality"); + ChoiceParam resolution = ChoiceParam(&_printerAttrs, &jobAttrs, IppMsg::Resolution, "printer-resolution"); + ChoiceParam scaling = ChoiceParam(&_printerAttrs, &jobAttrs, IppMsg::Keyword, "print-scaling"); + + ChoiceParam documentFormat = ChoiceParam(&_printerAttrs, &opAttrs, IppMsg::MimeMediaType, "document-format"); + + ChoiceParam mediaType = ChoiceParam(&_printerAttrs, &jobAttrs, IppMsg::Keyword, "media-type", "media-col"); + ChoiceParam mediaSource = ChoiceParam(&_printerAttrs, &jobAttrs, IppMsg::Keyword, "media-source", "media-col"); + ChoiceParam outputBin = ChoiceParam(&_printerAttrs, &jobAttrs, IppMsg::Keyword, "output-bin"); + + ChoiceParam topMargin = ChoiceParam(&_printerAttrs, &jobAttrs, IppMsg::Integer, "media-top-margin", "media-col"); + ChoiceParam bottomMargin = ChoiceParam(&_printerAttrs, &jobAttrs, IppMsg::Integer, "media-bottom-margin", "media-col"); + ChoiceParam leftMargin = ChoiceParam(&_printerAttrs, &jobAttrs, IppMsg::Integer, "media-left-margin", "media-col"); + ChoiceParam rightMargin = ChoiceParam(&_printerAttrs, &jobAttrs, IppMsg::Integer, "media-right-margin", "media-col"); + + IppParameters& operator=(const IppParameters& other) + { + opAttrs = other.opAttrs; + jobAttrs = other.jobAttrs; + printParams = other.printParams; + _printerAttrs = other._printerAttrs; + return *this; + }; + + typedef std::optional Error; + + Error finalize(std::string inputFormat, int pages=0); + + typedef std::pair ConvertKey; + typedef std::function ConvertFun; + + ConvertFun Pdf2Printable = [](std::string inFileName, WriteFun writeFun, const IppParameters& ippp, ProgressFun progressFun, bool verbose) + { + /*todo result*/ + pdf_to_printable(inFileName, writeFun, ippp.printParams, progressFun, verbose); + return Error(); + }; + + ConvertFun Baselinify = [](std::string inFileName, WriteFun writeFun, const IppParameters&, ProgressFun progressFun, bool) + { + InBinFile in(inFileName); + Bytestream inBts(in); + Bytestream baselinified; + baselinify(inBts, baselinified); + writeFun(baselinified.raw(), baselinified.size()); + progressFun(1, 1); + return Error(); + }; + + std::map Pipelines {{ConvertKey {PDF, PDF}, Pdf2Printable}, + {ConvertKey {PDF, Postscript}, Pdf2Printable}, + {ConvertKey {PDF, PWG}, Pdf2Printable}, + {ConvertKey {PDF, URF}, Pdf2Printable}, + {ConvertKey {JPEG, JPEG}, Baselinify}}; + + // TODO + // Additional formats + // error when no server + + IppAttrs opAttrs; + IppAttrs jobAttrs; + PrintParameters printParams; + +private: + + std::string determineTargetFormat(std::string inputFormat); + bool isImage(std::string format); + + void adjustRasterSettings(int pages); + + + static const std::string OctetStream; + + static const std::string PDF; + static const std::string Postscript; + static const std::string PWG; + static const std::string URF; + + static const std::string JPEG; + + IppAttrs _printerAttrs; + +}; + +#endif // IPPPARAMETERS_H diff --git a/lib/list.h b/lib/list.h new file mode 100644 index 0000000..a289056 --- /dev/null +++ b/lib/list.h @@ -0,0 +1,32 @@ +#ifndef LIST_H +#define LIST_H + +#include + +template +class List: public std::list +{ +public: + using std::list::list; + + T takeFront() + { + T tmp = std::list::front(); + std::list::pop_front(); + return tmp; + } + + bool contains(const T elem) const + { + for(const T& e : *this) + { + if(e == elem) + { + return true; + } + } + return false; + } +}; + +#endif // LIST_H diff --git a/lib/polymorph.h b/lib/polymorph.h new file mode 100644 index 0000000..9ff2a65 --- /dev/null +++ b/lib/polymorph.h @@ -0,0 +1,28 @@ +#ifndef POLYMORPH_H +#define POLYMORPH_H + +#include + +template +class Polymorph : public std::variant +{ +public: + using std::variant::variant; + + template + bool is() const + { + return std::holds_alternative(*this); + } + + template + T get() const + { + return std::get(*this); + } + +private: + +}; + +#endif // POLYMORPH_H diff --git a/lib/printparameters.h b/lib/printparameters.h index 482d58b..4ff8f31 100644 --- a/lib/printparameters.h +++ b/lib/printparameters.h @@ -110,8 +110,8 @@ class PrintParameters bool antiAlias = false; std::string paperSizeName = "iso_a4_210x297mm"; - size_t hwResW = 300; - size_t hwResH = 300; + uint32_t hwResW = 300; + uint32_t hwResH = 300; enum PaperSizeUnits { diff --git a/tests/Makefile b/tests/Makefile index f785a72..e6d7215 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -12,7 +12,7 @@ test.o: test.cpp %.o: %.cpp $(CXX) -c $(CXXFLAGS) $< -test: bytestream.o printparameters.o pwg2ppm.o test.o +test: bytestream.o ippparameters.o printparameters.o pwg2ppm.o ippmsg.o ippattr.o test.o $(CXX) $(CXXFLAGS) $^ $(LDFLAGS) -o $@ clean: diff --git a/tests/test.cpp b/tests/test.cpp index edfec63..4cc2b9b 100644 --- a/tests/test.cpp +++ b/tests/test.cpp @@ -6,6 +6,8 @@ #include "printparameters.h" #include "argget.h" #include "lthread.h" +#include "ippmsg.h" +#include "ippparameters.h" #include using namespace std; @@ -1230,3 +1232,639 @@ TEST(lthread) proceed = true; ltr.await(); } + +TEST(ippattr) +{ + + IppAttr attr1(IppMsg::BeginCollection, IppCollection{ { "key", IppAttr(7, 42) } } ); + + // TODO: Why does it need a double-braced list here? + // (it isn't the IppValue/Polymorph) + IppAttr attr2(IppMsg::Keyword, IppOneSetOf {{42}}); + ASSERT(attr2.isList()); + ASSERT(attr2.get().front().is()); + ASSERT(attr2.asList().front().is()); + ASSERT(attr2.asList().front().get() == 42); + + IppAttr attr3(IppMsg::Enum, 42); + ASSERT_FALSE(attr3.isList()); + ASSERT(attr3.get() == 42); + ASSERT(attr3.asList().front().is()); + ASSERT(attr3.asList().front().get() == 42); + +} + +TEST(ippparameters_support) +{ + IppAttrs printerAttrs = + {{"sides-default", IppAttr(IppMsg::Keyword, "one-sided")}, + {"sides-supported", IppAttr(IppMsg::Keyword, IppOneSetOf {"one-sided", + "two-sided-long-edge", + "two-sided-short-edge"})}, + {"media-default", IppAttr(IppMsg::Keyword, "iso_a4_210x297mm")}, + {"media-supported", IppAttr(IppMsg::Keyword, IppOneSetOf {"iso_a4_210x297mm", + "na_letter_8.5x11in"})}, + {"media-ready", IppAttr(IppMsg::Keyword, "iso_a4_210x297mm")}, + {"copies-default", IppAttr(IppMsg::Integer, 1)}, + {"copies-supported", IppAttr(IppMsg::IntegerRange, IppIntRange {1, 999})}, + {"multiple-document-handling-default", IppAttr(IppMsg::Keyword, "separate-documents-uncollated-copies")}, + {"multiple-document-handling-supported", IppAttr(IppMsg::Keyword, IppOneSetOf {"single-document", + "separate-documents-uncollated-copies", + "separate-documents-collated-copies"})}, + {"page-ranges-supported", IppAttr(IppMsg::Boolean, false)}, + {"number-up-default", IppAttr(IppMsg::Integer, 1)}, + {"number-up-supported", IppAttr(IppMsg::Integer, IppOneSetOf{1, 2, 4})}, + {"print-color-mode-default", IppAttr(IppMsg::Keyword, "auto")}, + {"print-color-mode-supported", IppAttr(IppMsg::Keyword, IppOneSetOf {"auto", "color", "monochrome"})}, + {"print-quality-default", IppAttr(IppMsg::Enum, 4)}, + {"print-quality-supported", IppAttr(IppMsg::Enum, IppOneSetOf {3, 4, 5})}, + {"printer-resolution-default", IppAttr(IppMsg::Resolution, IppResolution {600, 600, 3})}, + {"printer-resolution-supported", IppAttr(IppMsg::Resolution, IppOneSetOf {IppResolution {600, 600, 3}, + IppResolution {600, 1200, 3}})}, + {"document-format-default", IppAttr(IppMsg::Keyword, "application/octet-stream")}, + {"document-format-supported", IppAttr(IppMsg::Keyword, IppOneSetOf {"application/octet-stream", + "image/urf", + "image/pwg-raster", + "application/pdf"})}, + {"print-scaling-default", IppAttr(IppMsg::Keyword, "auto")}, + {"print-scaling-supported", IppAttr(IppMsg::Keyword, IppOneSetOf {"auto", "fill", "fit"})}, + {"media-type-default", IppAttr(IppMsg::Keyword, "stationery")}, + {"media-type-supported", IppAttr(IppMsg::Keyword, IppOneSetOf {"stationery", "cardstock", "labels"})}, + {"media-source-default", IppAttr(IppMsg::Keyword, "auto")}, + {"media-source-supported", IppAttr(IppMsg::Keyword, IppOneSetOf {"auto", "envelope", "manual", "tray-1"})}, + {"output-bin-default", IppAttr(IppMsg::Keyword, "face-down")}, + {"output-bin-supported", IppAttr(IppMsg::Keyword, "face-down")}, + + {"media-top-margin-default", IppAttr(IppMsg::Integer, 1)}, + {"media-top-margin-supported", IppAttr(IppMsg::Integer, IppOneSetOf {1, 2, 3, 4})}, + {"media-bottom-margin-default", IppAttr(IppMsg::Integer, 2)}, + {"media-bottom-margin-supported", IppAttr(IppMsg::Integer, IppOneSetOf {2, 3, 4})}, + {"media-left-margin-default", IppAttr(IppMsg::Integer, 3)}, + {"media-left-margin-supported", IppAttr(IppMsg::Integer, IppOneSetOf {3, 4})}, + {"media-right-margin-default", IppAttr(IppMsg::Integer, 4)}, + {"media-right-margin-supported", IppAttr(IppMsg::Integer, 4)}, + }; + + IppMsg getAttrsMsg(0, IppAttrs(), IppAttrs(), 1, 1, printerAttrs); + Bytestream encoded = getAttrsMsg.encode(); + IppMsg msg2(encoded); + + ASSERT(printerAttrs == msg2.getPrinterAttrs()); + + encoded -= encoded.size(); + IppMsg::setReqId(1); // Revert automatic global incrementation of ReqId. + Bytestream encoded2 = msg2.encode(); + ASSERT(encoded2 == encoded); + + IppParameters ip(printerAttrs); + + ASSERT(ip.sides.isSupported()); + ASSERT(ip.sides.getDefault() == "one-sided"); + ASSERT(ip.sides.getSupported() == List({"one-sided", + "two-sided-long-edge", + "two-sided-short-edge"})); + + ASSERT(ip.media.isSupported()); + ASSERT(ip.media.getDefault() == "iso_a4_210x297mm"); + ASSERT(ip.media.getSupported() == List({"iso_a4_210x297mm", "na_letter_8.5x11in"})); + // A single choice was not wrapped in OneSetOf, but still returned as a list. + ASSERT(ip.media.getPreferred() == List({"iso_a4_210x297mm"})); + + ASSERT(ip.copies.isSupported()); + ASSERT(ip.copies.getDefault() == 1); + ASSERT(ip.copies.getSupportedMin() == 1); + ASSERT(ip.copies.getSupportedMax() == 999); + + ASSERT(ip.multipleDocumentHandling.isSupported()); + ASSERT(ip.multipleDocumentHandling.getDefault() == "separate-documents-uncollated-copies"); + ASSERT(ip.multipleDocumentHandling.getSupported() == List({"single-document", + "separate-documents-uncollated-copies", + "separate-documents-collated-copies"})); + + // Maybe reinstate abstraction for these? + + // // Setting a crap default value... + // printerAttrs.insert_or_assign("multiple-document-handling-default", IppAttr(IppMsg::Keyword, "single-document")); + // ip = IppParameters(printerAttrs); + // ASSERT(ip.getCollatedCopiesDefault() == false); + + // // And supported... + // printerAttrs.insert_or_assign("multiple-document-handling-default", IppAttr(IppMsg::Keyword, "separate-documents-collated-copies")); + // ip = IppParameters(printerAttrs); + // ASSERT(ip.getCollatedCopiesDefault() == true); + + ASSERT(ip.pageRanges.isSupported()); + ASSERT(ip.pageRanges.getSupported() == false); + + ASSERT(ip.numberUp.isSupported()); + ASSERT(ip.numberUp.getDefault() == 1); + ASSERT(ip.numberUp.getSupported() == List({1, 2, 4})); + + // number-up-supported may be a range too... + printerAttrs.insert_or_assign("number-up-supported", IppAttr(IppMsg::IntegerRange, IppIntRange {1, 4})); + + ip = IppParameters(printerAttrs); + + ASSERT(ip.numberUp.isSupported()); + ASSERT(ip.numberUp.getDefault() == 1); + ASSERT(ip.numberUp.getSupported() == List({1, 2, 3, 4})); + + ASSERT(ip.colorMode.isSupported()); + ASSERT(ip.colorMode.getDefault() == "auto"); + ASSERT(ip.colorMode.getSupported() == List({"auto", "color", "monochrome"})); + + ASSERT(ip.printQuality.isSupported()); + ASSERT(ip.printQuality.getDefault() == 4); + ASSERT(ip.printQuality.getSupported() == List({3, 4, 5})); + + ASSERT(ip.resolution.isSupported()); + ASSERT(ip.resolution.getDefault() == IppResolution({600, 600, 3})); + ASSERT(ip.resolution.getSupported() == List({{600, 600, 3}, {600, 1200, 3}})); + + ASSERT(ip.scaling.isSupported()); + ASSERT(ip.scaling.getDefault() == "auto"); + ASSERT(ip.scaling.getSupported() == List({"auto", "fill", "fit"})); + + ASSERT(ip.documentFormat.isSupported()); + ASSERT(ip.documentFormat.getDefault() == "application/octet-stream"); + ASSERT(ip.documentFormat.getSupported() == List({"application/octet-stream", + "image/urf", + "image/pwg-raster", + "application/pdf"})); + + ASSERT(ip.mediaType.isSupported()); + ASSERT(ip.mediaType.getDefault() == "stationery"); + ASSERT(ip.mediaType.getSupported() == List({"stationery", "cardstock", "labels"})); + + ASSERT(ip.mediaSource.isSupported()); + ASSERT(ip.mediaSource.getDefault() == "auto"); + ASSERT(ip.mediaSource.getSupported() == List({"auto", "envelope", "manual", "tray-1"})); + + ASSERT(ip.outputBin.isSupported()); + ASSERT(ip.outputBin.getDefault() == "face-down"); + ASSERT(ip.outputBin.getSupported() == List({"face-down"})); + + ASSERT(ip.topMargin.isSupported()); + ASSERT(ip.topMargin.getDefault() == 1); + ASSERT(ip.topMargin.getSupported() == List({1, 2, 3, 4})); + + ASSERT(ip.bottomMargin.isSupported()); + ASSERT(ip.bottomMargin.getDefault() == 2); + ASSERT(ip.bottomMargin.getSupported() == List({2, 3, 4})); + + ASSERT(ip.leftMargin.isSupported()); + ASSERT(ip.leftMargin.getDefault() == 3); + ASSERT(ip.leftMargin.getSupported() == List({3, 4})); + + ASSERT(ip.rightMargin.isSupported()); + ASSERT(ip.rightMargin.getDefault() == 4); + ASSERT(ip.rightMargin.getSupported() == List({4})); + + // TODO: + // - additional formats + // - What to do on exceptions? + + // getters (and clampers) for pwg and urf resolutions supported + +} + +TEST(ippparameters_empty) +{ + IppAttrs printerAttrs; + IppParameters ip(printerAttrs); + + ASSERT_FALSE(ip.sides.isSupported()); + ASSERT(ip.sides.getDefault() == ""); + ASSERT(ip.sides.getSupported() == List()); + + ASSERT_FALSE(ip.media.isSupported()); + ASSERT(ip.media.getDefault() == ""); + ASSERT(ip.media.getSupported() == List()); + ASSERT(ip.media.getPreferred() == List()); + + ASSERT_FALSE(ip.copies.isSupported()); + ASSERT(ip.copies.getDefault() == 0); + ASSERT(ip.copies.getSupportedMin() == 0); + ASSERT(ip.copies.getSupportedMax() == 0); + + ASSERT_FALSE(ip.multipleDocumentHandling.isSupported()); + ASSERT(ip.multipleDocumentHandling.getDefault() == ""); + ASSERT(ip.multipleDocumentHandling.getSupported() == List()); + + ASSERT_FALSE(ip.pageRanges.isSupported()); + ASSERT(ip.pageRanges.getSupported() == false); + + ASSERT_FALSE(ip.numberUp.isSupported()); + ASSERT(ip.numberUp.getDefault() == 0); + ASSERT(ip.numberUp.getSupported() == List()); + + ASSERT_FALSE(ip.colorMode.isSupported()); + ASSERT(ip.colorMode.getDefault() == ""); + ASSERT(ip.colorMode.getSupported() == List()); + + ASSERT_FALSE(ip.printQuality.isSupported()); + ASSERT(ip.printQuality.getDefault() == 0); + ASSERT(ip.printQuality.getSupported() == List()); + + ASSERT_FALSE(ip.resolution.isSupported()); + ASSERT(ip.resolution.getDefault() == IppResolution()); + ASSERT(ip.resolution.getSupported() == List()); + + ASSERT_FALSE(ip.scaling.isSupported()); + ASSERT(ip.scaling.getDefault() == ""); + ASSERT(ip.scaling.getSupported() == List()); + + ASSERT_FALSE(ip.documentFormat.isSupported()); + ASSERT(ip.documentFormat.getDefault() == ""); + ASSERT(ip.documentFormat.getSupported() == List()); + + ASSERT_FALSE(ip.mediaType.isSupported()); + ASSERT(ip.mediaType.getDefault() == ""); + ASSERT(ip.mediaType.getSupported() == List()); + + ASSERT_FALSE(ip.mediaSource.isSupported()); + ASSERT(ip.mediaSource.getDefault() == ""); + ASSERT(ip.mediaSource.getSupported() == List()); + + ASSERT_FALSE(ip.outputBin.isSupported()); + ASSERT(ip.outputBin.getDefault() == ""); + ASSERT(ip.outputBin.getSupported() == List()); + + ASSERT_FALSE(ip.topMargin.isSupported()); + ASSERT(ip.topMargin.getDefault() == 0); + ASSERT(ip.topMargin.getSupported() == List()); + + ASSERT_FALSE(ip.bottomMargin.isSupported()); + ASSERT(ip.bottomMargin.getDefault() == 0); + ASSERT(ip.bottomMargin.getSupported() == List()); + + ASSERT_FALSE(ip.leftMargin.isSupported()); + ASSERT(ip.leftMargin.getDefault() == 0); + ASSERT(ip.leftMargin.getSupported() == List()); + + ASSERT_FALSE(ip.rightMargin.isSupported()); + ASSERT(ip.rightMargin.getDefault() == 0); + ASSERT(ip.rightMargin.getSupported() == List()); + +} + +TEST(ippparameters_set) +{ + IppAttrs printerAttrs; + IppParameters ip(printerAttrs); + + IppAttrs jobAttrs; + + ip.sides.set("two-sided"); + ASSERT(ip.sides.isSet()); + ASSERT(ip.sides.get() == "two-sided"); + jobAttrs.insert_or_assign("sides", IppAttr(IppMsg::Keyword, "two-sided")); + ASSERT(ip.jobAttrs == jobAttrs); + + ip.media.set("iso_a4_210x297mm"); + ASSERT(ip.media.isSet()); + ASSERT(ip.media.get() == "iso_a4_210x297mm"); + jobAttrs.insert_or_assign("media", IppAttr(IppMsg::Keyword, "iso_a4_210x297mm")); + ASSERT(ip.jobAttrs == jobAttrs); + + ip.copies.set(42); + ASSERT(ip.copies.isSet()); + ASSERT(ip.copies.get() == 42); + jobAttrs.insert_or_assign("copies", IppAttr(IppMsg::Integer, 42)); + ASSERT(ip.jobAttrs == jobAttrs); + + ip.multipleDocumentHandling.set("separate-documents-collated-copies"); + ASSERT(ip.multipleDocumentHandling.isSet()); + ASSERT(ip.multipleDocumentHandling.get() == "separate-documents-collated-copies"); + jobAttrs.insert_or_assign("multiple-document-handling", IppAttr(IppMsg::Keyword, "separate-documents-collated-copies")); + ASSERT(ip.jobAttrs == jobAttrs); + + // TODO: list-of-ranges + ip.pageRanges.set({IppIntRange {17, 42}}); + ASSERT(ip.pageRanges.isSet()); + ASSERT((ip.pageRanges.get() == IppOneSetOf {IppIntRange {17, 42}})); + jobAttrs.insert_or_assign("page-ranges", IppAttr(IppMsg::IntegerRange, IppOneSetOf {IppIntRange {17, 42}})); + ASSERT(ip.jobAttrs == jobAttrs); + + ip.numberUp.set(8); + ASSERT(ip.numberUp.isSet()); + ASSERT(ip.numberUp.get() == 8); + jobAttrs.insert_or_assign("number-up", IppAttr(IppMsg::Integer, 8)); + ASSERT(ip.jobAttrs == jobAttrs); + + ip.colorMode.set("monochrome"); + ASSERT(ip.colorMode.isSet()); + ASSERT(ip.colorMode.get() == "monochrome"); + jobAttrs.insert_or_assign("print-color-mode", IppAttr(IppMsg::Keyword, "monochrome")); + ASSERT(ip.jobAttrs == jobAttrs); + + ip.printQuality.set(5); + ASSERT(ip.printQuality.isSet()); + ASSERT(ip.printQuality.get() == 5); + jobAttrs.insert_or_assign("print-quality", IppAttr(IppMsg::Integer, 5)); + ASSERT(ip.jobAttrs == jobAttrs); + + ip.resolution.set(IppResolution {600, 600, 3}); + ASSERT(ip.resolution.isSet()); + ASSERT((ip.resolution.get() == IppResolution {600, 600, 3})); + jobAttrs.insert_or_assign("printer-resolution", IppAttr(IppMsg::Resolution, IppResolution {600, 600, 3})); + ASSERT(ip.jobAttrs == jobAttrs); + + ip.scaling.set("auto"); + ASSERT(ip.scaling.isSet()); + ASSERT(ip.scaling.get() == "auto"); + jobAttrs.insert_or_assign("print-scaling", IppAttr(IppMsg::Keyword, "auto")); + ASSERT(ip.jobAttrs == jobAttrs); + + ip.documentFormat.set("application/pdf"); + ASSERT(ip.documentFormat.isSet()); + ASSERT(ip.documentFormat.get() == "application/pdf"); + ASSERT((ip.opAttrs == IppAttrs {{"document-format", IppAttr(IppMsg::MimeMediaType, "application/pdf")}})); + ASSERT(ip.jobAttrs == jobAttrs); + + ip.outputBin.set("face-down"); + ASSERT(ip.outputBin.isSet()); + ASSERT(ip.outputBin.get() == "face-down"); + jobAttrs.insert_or_assign("output-bin", IppAttr(IppMsg::Keyword, "face-down")); + ASSERT(ip.jobAttrs == jobAttrs); + + ip.mediaType.set("stationery"); + ASSERT(ip.mediaType.isSet()); + ASSERT(ip.mediaType.get() == "stationery"); + ip.mediaSource.set("main"); + ASSERT(ip.mediaSource.isSet()); + ASSERT(ip.mediaSource.get() == "main"); + ip.topMargin.set(1); + ASSERT(ip.topMargin.isSet()); + ASSERT(ip.topMargin.get() == 1); + ip.bottomMargin.set(2); + ASSERT(ip.bottomMargin.isSet()); + ASSERT(ip.bottomMargin.get() == 2); + ip.leftMargin.set(3); + ASSERT(ip.leftMargin.isSet()); + ASSERT(ip.leftMargin.get() == 3); + ip.rightMargin.set(4); + ASSERT(ip.rightMargin.isSet()); + ASSERT(ip.rightMargin.get() == 4); + + IppCollection mediaCol {{"media-type", IppAttr(IppMsg::Keyword, "stationery")}, + {"media-source", IppAttr(IppMsg::Keyword, "main")}, + {"media-top-margin", IppAttr(IppMsg::Integer, 1)}, + {"media-bottom-margin", IppAttr(IppMsg::Integer, 2)}, + {"media-left-margin", IppAttr(IppMsg::Integer, 3)}, + {"media-right-margin", IppAttr(IppMsg::Integer, 4)}}; + + jobAttrs.insert_or_assign("media-col", IppAttr(IppMsg::BeginCollection, mediaCol)); + + ASSERT(ip.jobAttrs == jobAttrs); + +} + +TEST(finalize) +{ + IppAttrs printerAttrs = + {{"sides-default", IppAttr(IppMsg::Keyword, "one-sided")}, + {"sides-supported", IppAttr(IppMsg::Keyword, IppOneSetOf {"one-sided", + "two-sided-long-edge", + "two-sided-short-edge"})}, + {"media-default", IppAttr(IppMsg::Keyword, "iso_a4_210x297mm")}, + {"media-supported", IppAttr(IppMsg::Keyword, IppOneSetOf {"iso_a4_210x297mm", + "na_letter_8.5x11in"})}, + {"media-ready", IppAttr(IppMsg::Keyword, "iso_a4_210x297mm")}, + {"copies-default", IppAttr(IppMsg::Integer, 1)}, + {"copies-supported", IppAttr(IppMsg::IntegerRange, IppIntRange {1, 999})}, + {"multiple-document-handling-default", IppAttr(IppMsg::Keyword, "separate-documents-uncollated-copies")}, + {"multiple-document-handling-supported", IppAttr(IppMsg::Keyword, IppOneSetOf {"single-document", + "separate-documents-uncollated-copies", + "separate-documents-collated-copies"})}, + {"page-ranges-supported", IppAttr(IppMsg::Boolean, false)}, + {"number-up-default", IppAttr(IppMsg::Integer, 1)}, + {"number-up-supported", IppAttr(IppMsg::Integer, IppOneSetOf{1, 2, 4})}, + {"print-color-mode-default", IppAttr(IppMsg::Keyword, "auto")}, + {"print-color-mode-supported", IppAttr(IppMsg::Keyword, IppOneSetOf {"auto", "color", "monochrome"})}, + {"print-quality-default", IppAttr(IppMsg::Enum, 4)}, + {"print-quality-supported", IppAttr(IppMsg::Enum, IppOneSetOf {3, 4, 5})}, + {"printer-resolution-default", IppAttr(IppMsg::Resolution, IppResolution {600, 600, 3})}, + {"printer-resolution-supported", IppAttr(IppMsg::Resolution, IppOneSetOf {IppResolution {600, 600, 3}, + IppResolution {600, 1200, 3}})}, + {"document-format-default", IppAttr(IppMsg::Keyword, "application/octet-stream")}, + {"document-format-supported", IppAttr(IppMsg::Keyword, IppOneSetOf {"application/octet-stream", + "image/urf", + "image/pwg-raster", + "application/pdf"})}, + {"print-scaling-default", IppAttr(IppMsg::Keyword, "auto")}, + {"print-scaling-supported", IppAttr(IppMsg::Keyword, IppOneSetOf {"auto", "fill", "fit"})}, + {"media-type-default", IppAttr(IppMsg::Keyword, "stationery")}, + {"media-type-supported", IppAttr(IppMsg::Keyword, IppOneSetOf {"stationery", "cardstock", "labels"})}, + {"media-source-default", IppAttr(IppMsg::Keyword, "auto")}, + {"media-source-supported", IppAttr(IppMsg::Keyword, IppOneSetOf {"auto", "envelope", "manual", "tray-1"})}, + {"output-bin-default", IppAttr(IppMsg::Keyword, "face-down")}, + {"output-bin-supported", IppAttr(IppMsg::Keyword, "face-down")}, + + {"media-top-margin-default", IppAttr(IppMsg::Integer, 1)}, + {"media-top-margin-supported", IppAttr(IppMsg::Integer, IppOneSetOf {1, 2, 3, 4})}, + {"media-bottom-margin-default", IppAttr(IppMsg::Integer, 2)}, + {"media-bottom-margin-supported", IppAttr(IppMsg::Integer, IppOneSetOf {2, 3, 4})}, + {"media-left-margin-default", IppAttr(IppMsg::Integer, 3)}, + {"media-left-margin-supported", IppAttr(IppMsg::Integer, IppOneSetOf {3, 4})}, + {"media-right-margin-default", IppAttr(IppMsg::Integer, 4)}, + {"media-right-margin-supported", IppAttr(IppMsg::Integer, 4)}, + + {"pwg-raster-document-resolution-supported", IppAttr(IppMsg::Resolution, IppOneSetOf {IppResolution {300, 300, 3}, + IppResolution {600, 600, 3}})}, + {"pwg-raster-document-sheet-back", IppAttr(IppMsg::Keyword, "flipped")}, + {"document-format-varying-attributes", IppAttr(IppMsg::Keyword, "copies-supported")}, + {"urf-supported", IppAttr(IppMsg::Keyword, IppOneSetOf {"RS300-600", "DM3"})}, + + }; + + IppParameters ip(printerAttrs); + + // Margins - to be unset/ignored for PDF derivatives + ip.topMargin.set(1); + ip.bottomMargin.set(2); + ip.leftMargin.set(3); + ip.rightMargin.set(4); + + // To propagate to printParams + ip.resolution.set(IppResolution {600, 600, 3}); + + // To be kept in media since media-col is emptied when margins are removed + ip.media.set("na_letter_8.5x11in"); + + // To be removed in jobAttrs, but set in jobAttrs + ip.pageRanges.set({IppIntRange {17, 42}}); + + // To be kept and set in printParams + ip.sides.set("two-sided-long-edge"); + ip.printQuality.set(5); + ip.colorMode.set("monochrome"); + + ip.finalize("application/pdf"); + + ASSERT((ip.opAttrs == IppAttrs {{"document-format", IppAttr(IppMsg::MimeMediaType, "application/pdf")}})); + ASSERT((ip.jobAttrs == IppAttrs {{"printer-resolution", IppAttr(IppMsg::Resolution, IppResolution {600, 600, 3})}, + {"media", IppAttr(IppMsg::Keyword, "na_letter_8.5x11in")}, + {"sides", IppAttr(IppMsg::Keyword, "two-sided-long-edge")}, + {"print-quality", IppAttr(IppMsg::Enum, 5)}, + {"print-color-mode", IppAttr(IppMsg::Keyword, "monochrome")}})); + ASSERT(ip.printParams.format == PrintParameters::PDF); + ASSERT(ip.printParams.paperSizeName == "na_letter_8.5x11in"); + ASSERT(ip.printParams.hwResW == 600); + ASSERT(ip.printParams.hwResH == 600); + ASSERT((ip.printParams.pageRangeList == PageRangeList {{17, 42}})); + ASSERT(ip.printParams.duplexMode == PrintParameters::TwoSidedLongEdge); + ASSERT(ip.printParams.quality == PrintParameters::HighQuality); + ASSERT(ip.printParams.colorMode == PrintParameters::Gray8); + + // Forget choices + ip = IppParameters(printerAttrs); + + // With some random media-col setting, media-size moves to media-col too + ip.media.set("iso_a4_210x297mm"); + ip.mediaSource.set("tray-1"); + ip.mediaType.set("cardstock"); + + ip.finalize("application/pdf"); + + ASSERT((ip.opAttrs == IppAttrs {{"document-format", IppAttr(IppMsg::MimeMediaType, "application/pdf")}})); + IppCollection dimensions {{"x-dimension", IppAttr(IppMsg::Integer, 21000)}, + {"y-dimension", IppAttr(IppMsg::Integer, 29700)}}; + IppCollection mediaCol {{"media-type", IppAttr(IppMsg::Keyword, "cardstock")}, + {"media-size", IppAttr(IppMsg::BeginCollection, dimensions)}, + {"media-source", IppAttr(IppMsg::Keyword, "tray-1")}}; + ASSERT((ip.jobAttrs == IppAttrs {{"media-col", IppAttr(IppMsg::BeginCollection, mediaCol)}})); + + ASSERT(ip.printParams.paperSizeName == "iso_a4_210x297mm"); + ASSERT(ip.printParams.mediaPosition == PrintParameters::Tray1); + ASSERT(ip.printParams.mediaType == "cardstock"); + + // Remove PDF support, forget choices + printerAttrs.insert_or_assign("document-format-supported", + IppAttr(IppMsg::Keyword, IppOneSetOf {"application/octet-stream", + "image/urf", + "image/pwg-raster"})); + ip = IppParameters(printerAttrs); + + // To be removed and clamped to 600x600 in printParams + ip.resolution.set(IppResolution {600, 1200, 3}); + + ip.copies.set(2); + + ip.finalize("application/pdf"); + + ASSERT((ip.opAttrs == IppAttrs {{"document-format", IppAttr(IppMsg::MimeMediaType, "image/pwg-raster")}})); + ASSERT((ip.jobAttrs == IppAttrs {})); + ASSERT(ip.printParams.format == PrintParameters::PWG); + ASSERT(ip.printParams.hwResW == 600); + ASSERT(ip.printParams.hwResH == 600); + ASSERT(ip.printParams.backXformMode == PrintParameters::Flipped); + ASSERT(ip.printParams.copies == 2); + + // Forget and do almost the same again, but with URF selected + ip = IppParameters(printerAttrs); + ip.documentFormat.set("image/urf"); + + // To be removed and clamped to 600x600 in printParams + ip.resolution.set(IppResolution {600, 1200, 3}); + + ip.copies.set(2); + + ip.finalize("application/pdf"); + + ASSERT((ip.opAttrs == IppAttrs {{"document-format", IppAttr(IppMsg::MimeMediaType, "image/urf")}})); + ASSERT((ip.jobAttrs == IppAttrs {})); + ASSERT(ip.printParams.format == PrintParameters::URF); + ASSERT(ip.printParams.hwResW == 600); + ASSERT(ip.printParams.hwResH == 600); + ASSERT(ip.printParams.backXformMode == PrintParameters::Rotated); + ASSERT(ip.printParams.copies == 2); + + // Add Postscript, forget settings + printerAttrs.insert_or_assign("document-format-supported", + IppAttr(IppMsg::Keyword, IppOneSetOf {"application/octet-stream", + "application/postscript", + "image/urf", + "image/pwg-raster"})); + ip = IppParameters(printerAttrs); + + // Margins - to be unset/ignored for Postscript + ip.topMargin.set(1); + ip.bottomMargin.set(2); + ip.leftMargin.set(3); + ip.rightMargin.set(4); + + ip.resolution.set(IppResolution {600, 600, 3}); + // To be kept for Postscript + ip.pageRanges.set({IppIntRange {17, 42}}); + + ip.finalize("application/postscript"); + + ASSERT((ip.opAttrs == IppAttrs {{"document-format", IppAttr(IppMsg::MimeMediaType, "application/postscript")}})); + ASSERT((ip.jobAttrs == IppAttrs {{"printer-resolution", IppAttr(IppMsg::Resolution, IppResolution {600, 600, 3})}, + {"page-ranges", IppAttr(IppMsg::IntegerRange, IppOneSetOf {IppIntRange {17, 42}})}})); + // PrintParameters only used for PDF input + ASSERT(ip.printParams.format == PrintParameters::Invalid); + + // Add JPEG, forget settings + printerAttrs.insert_or_assign("document-format-supported", + IppAttr(IppMsg::Keyword, IppOneSetOf {"application/octet-stream", + "image/jpeg", + "image/urf", + "image/pwg-raster"})); + ip = IppParameters(printerAttrs); + + // Margins - to be kept for images + ip.topMargin.set(1); + ip.bottomMargin.set(2); + ip.leftMargin.set(3); + ip.rightMargin.set(4); + + ip.resolution.set(IppResolution {600, 600, 3}); + + ip.finalize("image/jpeg"); + + ASSERT((ip.opAttrs == IppAttrs {{"document-format", IppAttr(IppMsg::MimeMediaType, "image/jpeg")}})); + IppCollection mediaCol2 {{"media-top-margin", IppAttr(IppMsg::Integer, 1)}, + {"media-bottom-margin", IppAttr(IppMsg::Integer, 2)}, + {"media-left-margin", IppAttr(IppMsg::Integer, 3)}, + {"media-right-margin", IppAttr(IppMsg::Integer, 4)}}; + ASSERT((ip.jobAttrs == IppAttrs {{"printer-resolution", IppAttr(IppMsg::Resolution, IppResolution {600, 600, 3})}, + {"media-col", IppAttr(IppMsg::BeginCollection, mediaCol2)}})); + // PrintParameters only used for PDF input + ASSERT(ip.printParams.format == PrintParameters::Invalid); + + // Forget choices + ip = IppParameters(printerAttrs); + + // Margins - to be unset/ignored for rasters + ip.topMargin.set(1); + ip.bottomMargin.set(2); + ip.leftMargin.set(3); + ip.rightMargin.set(4); + + // To be changed for single-page document doing copies + ip.sides.set("two-sided-long-edge"); + ip.copies.set(2); + + ip.finalize("application/pdf", 1); + + ASSERT((ip.opAttrs == IppAttrs {{"document-format", IppAttr(IppMsg::MimeMediaType, "image/pwg-raster")}})); + ASSERT((ip.jobAttrs == IppAttrs {{"sides", IppAttr(IppMsg::Keyword, "one-sided")}})); + ASSERT(ip.printParams.format == PrintParameters::PWG); + ASSERT(ip.printParams.colorMode == PrintParameters::sRGB24); + + // Un-support color, forget choices + printerAttrs.insert_or_assign("print-color-mode-supported", + IppAttr(IppMsg::Keyword, IppOneSetOf {"auto", "monochrome"})); + ip = IppParameters(printerAttrs); + ip.finalize("application/pdf", 0); + + ASSERT((ip.opAttrs == IppAttrs {{"document-format", IppAttr(IppMsg::MimeMediaType, "image/pwg-raster")}})); + ASSERT(ip.printParams.format == PrintParameters::PWG); + ASSERT(ip.printParams.colorMode == PrintParameters::Gray8); // Since we don't support color + + // TODO test pseudo image-to-raster flow + // (get PrintParameters, and run finalize after...) + +} diff --git a/utils/ippposter.cpp b/utils/ippposter.cpp index 43b9438..20f7a51 100644 --- a/utils/ippposter.cpp +++ b/utils/ippposter.cpp @@ -1,34 +1,428 @@ #include #include #include +#include +#include +#include "argget.h" #include "curlrequester.h" +#include "ippmsg.h" +#include "ippparameters.h" +#include +#include -// WIP: use ArgGet and stream from stdin before it's official +#define HELPTEXT "FIXME" + +inline void print_error(std::string hint, std::string argHelp) +{ + std::cerr << hint << std::endl << std::endl << argHelp << std::endl << HELPTEXT << std::endl; +} + +IppAttrs opAttrs(std::string url) +{ + std::string name = getenv("USER"); + IppAttrs o + { + {"attributes-charset", IppAttr(IppMsg::Charset, "utf-8")}, + {"attributes-natural-language", IppAttr(IppMsg::NaturalLanguage, "en-us")}, + {"printer-uri", IppAttr(IppMsg::Uri, url)}, + {"requesting-user-name", IppAttr(IppMsg::NameWithoutLanguage, name)}, + }; + return o; +} + +template +std::ostream& operator<<(std::ostream& os, List bl) +{ + if(bl.size() > 0) + { + os << "[" << bl.takeFront(); + for(T b : bl) + { + os << ", " << b; + } + os << "]"; + } + else + { + os << "[]"; + } + return os; +} + +int doPrint(std::string addr, std::string inFile, std::string inFormat, Bytestream hdr, PrintParameters params, bool verbose); int main(int argc, char** argv) { - if(argc != 3) + bool help = false; + bool verbose = false; + std::string pages; + int copies; + std::string paperSize; + uint32_t hwRes; + uint32_t hwResX; + uint32_t hwResY; + bool duplex = false; + bool tumble = false; + std::string colorMode; + int quality; + + std::string scaling; + std::string format; + std::string mediaType; + std::string mediaSource; + std::string outputBin; + + int topMargin; + int bottomMargin; + int leftMargin; + int rightMargin; + + std::string addr; + std::string infile; + + SwitchArg helpOpt(help, {"-h", "--help"}, "Print this help text"); + SwitchArg verboseOpt(verbose, {"-v", "--verbose"}, "Be verbose, print headers and progress"); + SwitchArg pagesOpt(pages, {"-p", "--pages"}, "What pages to process, e.g.: 1,17-42"); + SwitchArg copiesOpt(copies, {"--copies"}, "Number of copies to output"); + + SwitchArg paperSizeOpt(paperSize, {"--paper-size"}, "Paper size to output, e.g.: iso_a4_210x297mm"); + SwitchArg resolutionOpt(hwRes, {"-r", "--resolution"}, "Resolution (in DPI) for rasterization"); + SwitchArg resolutionXOpt(hwResX, {"-rx", "--resolution-x"}, "Resolution (in DPI) for rasterization, x-axis"); + SwitchArg resolutionYOpt(hwResY, {"-ry", "--resolution-y"}, "Resolution (in DPI) for rasterization, y-axis"); + SwitchArg duplexOpt(duplex, {"-d", "--duplex"}, "Do duplex printing"); + SwitchArg tumbleOpt(tumble, {"-t", "--tumble"}, "Do tumbled duplex printing"); + + SwitchArg colorModeOpt(colorMode, {"-c", "--color-mode"}, "Color mode (as per IPP)"); + EnumSwitchArg qualityOpt(quality, {{"draft", PrintParameters::DraftQuality}, + {"normal", PrintParameters::NormalQuality}, + {"high", PrintParameters::HighQuality}}, + {"-q", "--quality"}, + "Quality (draft/normal/high)"); + + SwitchArg scalingOpt(scaling, {"--scaling"}, "Scaling (as per IPP)"); + SwitchArg formatOpt(format, {"--format"}, "Transfer format"); + SwitchArg mediaTypeOpt(mediaType, {"--media-type"}, "Media type (as per IPP)"); + SwitchArg mediaSourceOpt(mediaSource, {"--media-source"}, "Media source (as per IPP)"); + SwitchArg outputBinOpt(outputBin, {"--output-bin"}, "Output bin (as per IPP)"); + + SwitchArg topMarginOpt(topMargin, {"-tm", "--top-margin"}, "Top margin (as per IPP)"); + SwitchArg bottomMarginOpt(bottomMargin, {"-bm", "--bottom-margin"}, "Bottom margin (as per IPP)"); + SwitchArg leftMarginOpt(leftMargin, {"-lm", "--left-margin"}, "Left margin (as per IPP)"); + SwitchArg rightMarginOpt(rightMargin, {"-rm", "--right-margin"}, "Right margin (as per IPP)"); + + // SwitchArg antiAliasOpt(params.antiAlias, {"-aa", "--antaialias"}, "Enable antialiasing in rasterization"); + + PosArg addrArg(addr, "printer address"); + PosArg pdfArg(infile, "input file"); + + ArgGet args({&helpOpt, &verboseOpt, &pagesOpt, + &copiesOpt, &paperSizeOpt, &resolutionOpt, + &resolutionXOpt, &resolutionYOpt, &duplexOpt, &tumbleOpt, + &colorModeOpt, &qualityOpt, &scalingOpt, &formatOpt, + &mediaTypeOpt, &mediaSourceOpt, &outputBinOpt, + &topMarginOpt, &bottomMarginOpt, &leftMarginOpt, &rightMarginOpt}, + {&addrArg, &pdfArg}); + + bool correctArgs = args.get_args(argc, argv); + if(help) + { + std::cout << args.argHelp() << std::endl << HELPTEXT << std::endl; + return 0; + } + else if(!correctArgs) + { + print_error(args.errmsg(), args.argHelp()); + return 1; + } + + // TODO: ensure ipp address - convert to http for curl + IppAttrs getPrinterAttrsJobAttrs = opAttrs(addr); + IppMsg getPrinterAttrsMsg(IppMsg::GetPrinterAttrs, getPrinterAttrsJobAttrs); + Bytestream getPrinterAttrsEnc = getPrinterAttrsMsg.encode(); + + CurlIppPoster getPrinterAttrsReq(addr, getPrinterAttrsEnc, true, verbose); + getPrinterAttrsReq.write((char*)(getPrinterAttrsEnc.raw()), getPrinterAttrsEnc.size()); + Bytestream getPrinterAttrsResult; + CURLcode res0 = getPrinterAttrsReq.await(&getPrinterAttrsResult); + IppAttrs printerAttrs; + if(res0 == CURLE_OK) + { + IppMsg getPrinterAttrsResp(getPrinterAttrsResult); + printerAttrs = getPrinterAttrsResp.getPrinterAttrs(); + } + else + { + std::cerr << std::endl << "Could not get printer attributes: " << curl_easy_strerror(res0) << std::endl; + return 1; + } + + IppParameters ip(printerAttrs); + + // std::cerr << "sides: " << ip.sides.getDefault() << std::endl; + // std::cerr << ip.sides.getSupported() << std::endl << std::endl; + + // std::cerr << "media: " << ip.media.getDefault() << std::endl; + // std::cerr << ip.media.getSupported() << std::endl; + // std::cerr << ip.media.getPreferred() << std::endl << std::endl; + + // std::cerr << "copies: " << ip.copies.getDefault() << std::endl; + // std::cerr << ip.copies.getSupportedMin() << " - " + // << ip.copies.getSupportedMax() << std::endl << std::endl; + + // std::cerr << "collated copies: " << ip.multipleDocumentHandling.getDefault() << std::endl; + // std::cerr << ip.multipleDocumentHandling.getSupported() << std::endl << std::endl; + + // std::cerr << "page ranges: " << ip.pageRanges.getSupported() << std::endl << std::endl; + + // std::cerr << "number up: " << ip.numberUp.getDefault() << std::endl; + // std::cerr << ip.numberUp.getSupported() << std::endl << std::endl; + + // std::cerr << "color: " << ip.colorMode.getDefault() << std::endl; + // std::cerr << ip.colorMode.getSupported() << std::endl << std::endl; + + // std::cerr << "quality: " << ip.printQuality.getDefault() << std::endl; + // std::cerr << ip.printQuality.getSupported() << std::endl << std::endl; + + // std::cerr << "resolution: " << ip.resolution.getDefault() << std::endl; + // std::cerr << ip.resolution.getSupported() << std::endl << std::endl; + + // std::cerr << "scaling: " << ip.scaling.getDefault() << std::endl; + // std::cerr << ip.scaling.getSupported() << std::endl << std::endl; + + // std::cerr << "format: " << ip.documentFormat.getDefault() << std::endl; + // std::cerr << ip.documentFormat.getSupported() << std::endl << std::endl; + + // std::cerr << "media type: " << ip.mediaType.getDefault() << std::endl; + // std::cerr << ip.mediaType.getSupported() << std::endl << std::endl; + + // std::cerr << "media source: " << ip.mediaSource.getDefault() << std::endl; + // std::cerr << ip.mediaSource.getSupported() << std::endl << std::endl; + + // std::cerr << "output bin: " << ip.outputBin.getDefault() << std::endl; + // std::cerr << ip.outputBin.getSupported() << std::endl << std::endl; + + // std::cerr << "media top margin: " << ip.topMargin.getDefault() << std::endl; + // std::cerr << ip.topMargin.getSupported() << std::endl << std::endl; + + // std::cerr << "media bottom margin: " << ip.bottomMargin.getDefault() << std::endl; + // std::cerr << ip.bottomMargin.getSupported() << std::endl << std::endl; + + // std::cerr << "media left margin: " << ip.leftMargin.getDefault() << std::endl; + // std::cerr << ip.leftMargin.getSupported() << std::endl << std::endl; + + // std::cerr << "media right margin: " << ip.rightMargin.getDefault() << std::endl; + // std::cerr << ip.rightMargin.getSupported() << std::endl << std::endl; + + + // Error on unsupported - add force-option + // check invalid/redundant combinations + + if(pagesOpt.isSet()) { + std::cerr << "TODO: pages"; return 1; } + if(copiesOpt.isSet()) + { + ip.copies.set(copies); + } + if(copiesOpt.isSet()) + { + ip.copies.set(copies); + } + if(paperSizeOpt.isSet()) + { + ip.media.set(paperSize); + } + if(resolutionOpt.isSet()) + { + ip.resolution.set(IppResolution {hwRes, hwRes, IppResolution::DPI}); + } + if(resolutionXOpt.isSet() && resolutionYOpt.isSet()) + { + ip.resolution.set(IppResolution {hwResX, hwResY, IppResolution::DPI}); + } + if(tumble) + { + ip.sides.set("two-sided-short-edge"); + } + if(duplex) + { + ip.sides.set("two-sided-long-edge"); + } + if(colorModeOpt.isSet()) + { + ip.colorMode.set(colorMode); + } + if(qualityOpt.isSet()) + { + ip.printQuality.set(quality); + } + if(scalingOpt.isSet()) + { + ip.scaling.set(scaling); + } + if(formatOpt.isSet()) + { + ip.documentFormat.set(format); + } + if(mediaTypeOpt.isSet()) + { + ip.mediaType.set(mediaType); + } + if(mediaSourceOpt.isSet()) + { + ip.mediaSource.set(mediaSource); + } + if(outputBinOpt.isSet()) + { + ip.outputBin.set(outputBin); + } + // Maybe add a single margin too... + if(topMarginOpt.isSet()) + { + ip.topMargin.set(topMargin); + } + if(bottomMarginOpt.isSet()) + { + ip.bottomMargin.set(bottomMargin); + } + if(leftMarginOpt.isSet()) + { + ip.leftMargin.set(leftMargin); + } + if(rightMarginOpt.isSet()) + { + ip.rightMargin.set(rightMargin); + } - std::string addr = argv[1]; - std::string inFile = argv[2]; + ip.finalize("application/pdf"); - std::ifstream ifs(inFile, std::ios::in | std::ios::binary); + List supportedOperations = printerAttrs.getList("operations-supported"); - Bytestream hdr(ifs); - Bytestream data(std::cin); + std::string fileName = std::filesystem::path(infile).filename(); - CurlIppStreamer req(addr); + if(supportedOperations.contains(IppMsg::CreateJob) && supportedOperations.contains(IppMsg::SendDocument)) + { + IppAttrs createJobOpAttrs = opAttrs(addr); + createJobOpAttrs.insert_or_assign("job-name", IppAttr {IppMsg::NameWithoutLanguage, fileName}); - req.write(hdr.raw(), hdr.size()); - req.write(data.raw(), data.size()); + IppMsg createJobMsg(IppMsg::CreateJob, createJobOpAttrs, ip.jobAttrs); + Bytestream createJobEnc = createJobMsg.encode(); - Bytestream result; + CurlIppPoster createJobReq(addr, createJobEnc, true, verbose); - req.await(&result); + createJobReq.write((const char*)createJobEnc.raw(), createJobEnc.size()); + Bytestream createJobResult; + CURLcode res = createJobReq.await(&createJobResult); - std::cerr << "resp: \n" << result.hexdump(result.size()) << std::endl; + if(res == CURLE_OK) + { + IppMsg createJobResp(createJobResult); + IppAttrs createJobRespJobAttrs; + if(!createJobResp.getJobAttrs().empty()) + { + createJobRespJobAttrs = createJobResp.getJobAttrs().front(); + } + if(createJobResp.getStatus() <= 0xff && createJobRespJobAttrs.has("job-id")) + { + int jobId = createJobRespJobAttrs.get("job-id"); + IppAttrs sendDocumentOpAttrs = opAttrs(addr); + sendDocumentOpAttrs.insert(ip.opAttrs.begin(), ip.opAttrs.end()); + sendDocumentOpAttrs.insert_or_assign("job-id", IppAttr {IppMsg::Integer, jobId}); + sendDocumentOpAttrs.insert_or_assign("last-document", IppAttr {IppMsg::Boolean, true}); + IppMsg sendDocumentMsg(IppMsg::SendDocument, sendDocumentOpAttrs); + return doPrint(addr, infile, "application/pdf", sendDocumentMsg.encode(), ip.printParams, verbose); + } + else + { + std::cerr << "Create job failed: " << createJobResp.getOpAttrs().get("status-message") << std::endl; + } + } + else + { + std::cerr << "Create job failed: " << curl_easy_strerror(res) << std::endl; + } + } + else + { + IppAttrs printJobOpAttrs = opAttrs(addr); + printJobOpAttrs.insert(ip.opAttrs.begin(), ip.opAttrs.end()); + printJobOpAttrs.insert_or_assign("job-name", IppAttr {IppMsg::NameWithoutLanguage, fileName}); + IppMsg printJobMsg(IppMsg::PrintJob, printJobOpAttrs, ip.jobAttrs); + return doPrint(addr, infile, "application/pdf", printJobMsg.encode(), ip.printParams, verbose); + } +} + + +int doPrint(std::string addr, std::string inFile, std::string inFormat, Bytestream hdr, PrintParameters params, bool verbose) +{ + int res = 0; + CurlIppStreamer cr(addr, true, verbose); + cr.write((char*)(hdr.raw()), hdr.size()); + if(params.format != PrintParameters::Invalid) + { + WriteFun writeFun([&cr](unsigned char const* buf, unsigned int len) -> bool + { + if(len == 0) + return true; + return cr.write((const char*)buf, len); + }); + + ProgressFun progressFun([](size_t page, size_t total) -> void + { + std::cerr << page << "/" << total << std::endl; + }); + + res = pdf_to_printable(inFile, writeFun, params, progressFun, verbose); + } + else + { + std::ifstream ifs; + std::istream* in; + + if(inFile == "-") + { + in = &std::cin; + } + else + { + ifs = std::ifstream(inFile, std::ios::in | std::ios::binary); + in = &ifs; + } + Bytestream inBts(*in); + + if(inFormat == "image/jpeg") + { + Bytestream baselinified; + baselinify(inBts, baselinified); + cr.write((char*)(baselinified.raw()), baselinified.size()); + } + else + { + cr.write((char*)(inBts.raw()), inBts.size()); + } + } + + if(!res) + { + Bytestream result; + CURLcode cres = cr.await(&result); + if(cres == CURLE_OK) + { + IppMsg response(result); + if(response.getStatus() > 0xff) + { + res = response.getStatus(); + std::cerr << "Print failed: " << response.getOpAttrs().get("status-message") << std::endl; + } + } + else + { + std::cerr << "Print failed: " << curl_easy_strerror(cres) << std::endl; + res = cres; + } + } + return res; } diff --git a/utils/pdf2printable_main.cpp b/utils/pdf2printable_main.cpp index 670ab23..b2e0a48 100644 --- a/utils/pdf2printable_main.cpp +++ b/utils/pdf2printable_main.cpp @@ -16,7 +16,7 @@ inline void print_error(std::string hint, std::string argHelp) std::cerr << hint << std::endl << std::endl << argHelp << std::endl << HELPTEXT << std::endl; } -inline bool ends_with(std::string s, std::string ending) +inline bool endsWith(std::string s, std::string ending) { if(ending.length() <= s.length()) { @@ -115,19 +115,19 @@ int main(int argc, char** argv) if(!formatOpt.isSet()) { - if(ends_with(outFileName, ".ps")) + if(endsWith(outFileName, ".ps")) { params.format = PrintParameters::Postscript; } - else if(ends_with(outFileName, ".pwg")) + else if(endsWith(outFileName, ".pwg")) { params.format = PrintParameters::PWG; } - else if(ends_with(outFileName, ".urf")) + else if(endsWith(outFileName, ".urf")) { params.format = PrintParameters::URF; } - else if(ends_with(outFileName, ".pdf")) + else if(endsWith(outFileName, ".pdf")) { params.format = PrintParameters::PDF; }