Hippy
Hippy is an Internet Printing Protocol (IPP) client implementation in Elixir for performing distributed printing over HTTP. In addition to printing documents, IPP also supports a number of other operations such as retrieving print job status, pausing jobs, resuming jobs, etc.
CUPS provides a standard IPP server out of the box, though some higher-end networked printers are also capable of providing their own server. That said, testing at this early stage has been 100% against CUPS, which is likely more tolerant of any outstanding abuses of the protocol in Hippy alpha versions. ;)
Obligatory Warning
This is currently very alpha-stage software and will be evolving quickly. Please only consider it for production use at your own risk.
Configuration
Hippy uses Req for HTTP and requires Elixir 1.15 or later. Extra Req options (timeouts, TLS settings, etc.) can be passed through application config:
config :hippy, req_options: [receive_timeout: 30_000]
Enum attributes are decoded to atoms for common attributes such as job-state and orientation-requested, and left as integers otherwise. To decode other enum attributes (e.g. vendor extensions), define an enum and register it by attribute name. Registered enums take precedence over the built-in ones, and xxx-default and xxx-supported use the enum registered for xxx:
defmodule MyApp.VendorState do
use Hippy.Protocol.Enum, %{idle: 0x01, busy: 0x02}
end
config :hippy, enums: %{
"x-vendor-state" => MyApp.VendorState,
"input-orientation-requested" => Hippy.Protocol.Orientation
}
Usage Examples
Print a PDF document
doc = File.read!("tps_report.pdf")
printer_uri = "http://localhost:631/printers/HP_Color_LaserJet"
Hippy.Operation.PrintJob.new(printer_uri, doc, job_name: "TPS Report", orientation: Hippy.Protocol.Orientation.portrait())
|> Hippy.send_operation()
{:ok,
%Hippy.Response{
job_attributes: [
[
{:uri, "job-uri", "ipp://localhost:631/jobs/47"},
{:integer, "job-id", 47},
{:enum, "job-state", :pending},
{:text_without_language, "job-state-message", ""},
{:keyword, "job-state-reasons", "none"}
]
],
operation_attributes: [
[
{:charset, "attributes-charset", "utf-8"},
{:natural_language, "attributes-natural-language", "en"}
]
],
request_id: 1,
status_code: :successful_ok,
version: %Hippy.IPPVersion{major: 1, minor: 1}
}}
Get recently completed jobs
Hippy.Operation.GetJobs.new("http://localhost:631/printers/HP_Color_LaserJet")
|> Hippy.send_operation()
{:ok,
%Hippy.Response{
job_attributes: [
[
{:name_without_language, "job-name", "TPS Report"},
{:name_without_language, "job-originating-user-name", "anonymous"},
{:integer, "job-id", 47},
{:enum, "job-state", :completed},
{:keyword, "job-state-reasons", "processing-to-stop-point"},
{:integer, "job-media-sheets-completed", 8}
],
[
{:name_without_language, "job-name", "Untitled Job"},
{:name_without_language, "job-originating-user-name", "anonymous"},
{:integer, "job-id", 46},
{:enum, "job-state", :completed},
{:keyword, "job-state-reasons", "processing-to-stop-point"},
{:integer, "job-media-sheets-completed", 2}
],
# ...
],
operation_attributes: [
[
{:charset, "attributes-charset", "utf-8"},
{:natural_language, "attributes-natural-language", "en"},
{:integer, "limit", 500}
]
],
request_id: 1,
status_code: :successful_ok,
version: %Hippy.IPPVersion{major: 1, minor: 1}
}}
Convert the response to a map for easier access by key
Note: Hippy.AttributeGroup.to_map/1 currently doesn't support nested attribute groups such as when returning multiple jobs through the Get-Jobs IPP operation. If given a list of attribute groups, instead of returning a list of maps, it will return the group at the head of the list. If you wish to access other groups in the list, you can use AttributeGroup.to_map/2 to provide an index for the nested group at the target index. This behavior may change for AttributeGroup.to_map/1 in the future, so if you intend to rely on a single map being returned, it would be advisable to use AttributeGroup.to_map(response.job_attributes, 0) in order to future-proof your code.
response = %Hippy.Response{
data: "",
job_attributes: [
[
{:uri, "job-uri", "ipp://localhost:631/jobs/47"},
{:integer, "job-id", 47},
{:enum, "job-state", :pending},
{:text_without_language, "job-state-message", ""},
{:keyword, "job-state-reasons", "none"}
]
],
operation_attributes: [
[
{:charset, "attributes-charset", "utf-8"},
{:natural_language, "attributes-natural-language", "en"}
]
],
printer_attributes: [],
request_id: 1,
status_code: :successful_ok,
unknown_attributes: [],
version: %Hippy.IPPVersion{major: 1, minor: 1}
}
Hippy.AttributeGroup.to_map(response.job_attributes)
%{
"job-id" => 47,
"job-state" => :pending,
"job-state-message" => "",
"job-state-reasons" => "none",
"job-uri" => "ipp://localhost:631/jobs/47"
}
Development
Run the unit tests with mix test.
Integration tests run Hippy against a real CUPS server in a throwaway Docker container (see test/integration/cups/). They are excluded by default and need a running Docker daemon:
mix test --only cups
Copyright and License
Copyright (c) 2018-2026 Jeff Smith
Hippy source code is licensed under the MIT License.