This is a fairly long article, so here is a short summary of what you will find in it:
The image layer interface is defined in module /pliant/graphic/image/prototype.pli
The simplest way of defining an image prototype is:
image_prototype 10 20 30 40 100 200 color_gamut:"rgb"
The first four parameters of image_prototype function are the left top right and bottom coordinates of the borders of the images, in millimeters.
Another way is to provide the image resolution rather than the number of pixels:
image_prototype 10 20 30 40 300 600 4 4 image_adjust_extend color_gamut:"rgb"
Here, 300 and 600 are the resolution in dpi (dot per inch), and the next to come double 4 specifies that the final number of columns and rows have to be a multiple of 4 (this can be important for some applications that do anti-aliasing, so group tiles of pixels in the end).
Various exact dimension versus resolution adjustments policies are:
A third, less frequently used method is to derive the new image prototype from an existing one. Assuming that 'p' is an existing image, we can get an image with the (roughly) same dimensions and pixels encoding, but at 300 dpi, through:
image_prototype p "resolution 300"
The various instructions that can be used to modify the image are 'margin' 'x0' 'y0' 'x1' 'y1' 'drop_transparency' 'size_x' 'size_y' 'resolution' (with one or two parameters) and 'antialiasing' (also with one or two parameters).
var Link:ImagePrototype img :> new ImagePixmap
'setup' is used to resize the image pixels grid according to the provided image prototype specifications. The last parameter of 'setup', here an empty string is used to provide extra configuration parameters, and we will see it later.
Please, first notice that an image must be a Pliant object, not a local variable, so writing something like:
var ImagePixmap img # Don't do that
var Link:ImagePixmap img :> new ImagePixmap
would be a bug.
Then, please also notice that reading pixels outside image boundary is a bug that will produce unpredictable result and is likely to just crash Pliant process. When issuing:
var Int x y count
The following assertion must be satisfied: x>=0 and x+count<=img:size_x and y>=0 and y<img:size_y and count>=0
If and only if the image has type 'ImagePixmap', then we can access pixels directly using 'pixel' method that returns the address of the specified pixel:
c := (img pixel 100 200) map ColorRGB888
which is the same as:
img read 100 200 1 addressof:c
Now, informations about the color model:
console img:gamut:pixel_size eol
See color models for extra informations about color encoding provided by a gamut.
One of these, the pixel size, is a so frequently used that it is copied inside the image header in order to be accessible without the gamut indirection:
console img:pixel_size eol
Lastly, just like for color models, images have a 'configure' and 'query' method that can be used to set of read special parameters of the image. The semantic of this example will be explained later:
img configure "shrink"
Now, the huge super fantastic incredible terrific ... two cents trick about image processing is the fact that 'read' and 'write' methods don't work on a single pixel, but on several pixels at once. The reason is that if 'read' was working on a single pixel, the glue code for connection the effective function, then finding the address of the pixel in memory would be very expensive in the end since applied to millions of pixels.
So, pushing the trick further, we introduce an even more efficient way to access pixels, that also avoid the copy implied by the 'read' and 'write'.
for (var Int y) 0 img:size_y
The parameters provided to 'write_map' are the position of the first pixel, just like with 'write', then the minimum and maximum number of pixels to write, and finally a local variable that will be set on return with the effective number of pixels mapped.
For not yet stunned people, there is also 'read_map' and 'read_unmap'.
Then we have fill for writing the same pixels several times (here 10 times):
var ColorRGB888 c := color rgb 128 128 128
When pasting an image in another one, with a rotation transformation, being able to read and write pixels only horizontally is a serious problem, so for this situation, 2 dimensions reading is also provided:
var Address a := img rectangle_read_map 100 200 (var Int x0) (var Int y0) (var Int x1) (var Int y1) (var Int step_x) (var Int step_y)
The two first parameters in 'rectangle_read_map' provide the point that we want to read.
In very few words, 'rectangle_read_map' says: I want to read this pixel; and the answer of it is: you can also read at once all the surrounding pixels in the provided range.
Now, about implementation, when you use an ImagePixmap uncompressed image, the image storage is not allocated as a single chunk because it might too easily overflow the address space on a 32 bits system, and also not allocated line by line because 'rectangle_read_map' would not work efficiently enough. The storage is allocated as tiles of several lines. The default size of a tile is 64 KB, but it can be adjusted through using 'tile_size' option in 'setup' method options parameter (or 'tile_y' to directly force the number of lines per tile). See 'In memory compressed images' paragraph bellow for examples of options passed to 'setup' method.
In order to ask Pliant to compress the image in memory, just use:
var Link:ImagePrototype img :> new ImagePacked
var Link:ImagePrototype img :> new ImagePixmap
An introduction to PACK4 encoding
Let's talk a little bit about PACK4 encoding.
It's implemented in /pliant/util/encoding/pack4.pli and is an improvement of packbits encoding (see Google for an introduction to packbits), working on pixels instead of bytes (I don't know who is the idiot that decided packbits would work on bytes as opposed to pixels) and containing four instructions instead of 2:
The third code is optimizing oversampled images.
Now, the nice properties of PACK4 are:
And the bad properties are:
Packed image implementation
Now, if you read the code in /pliant/graphic/image/packed.pli, you will see that it's not completely trivial, as opposed to the code handling uncompressed images, and the reason is that the code provides two features:
Let's explain all this with greater details.
The general idea is that a packed image is encoded as a set of tiles (each tile, excepts the ones on the border, contains tile_x columns and tile_y rows).
Each time access to a tile is requested due to methods 'read' or 'write' being called, the tile is automatically uncompressed if it was compressed.
Packed image specific features
Here are some extra features available only for in memory packed image:
First, you can read to content or write the content of a packed image to disk through:
var ExtendedStatus status := img fast_save "file:/tmp/test.packed"
var ExtendedStatus satus := img fast_load "file:/tmp/test.pack"
The difference between 'fast_save' and 'fast_load' and the standard 'load' and 'save' that we will see later is that there is no uncompression then recompression: titles are written directly to the disk so it can be significantly faster is some situations.
img configure "shrink"
will force all tiles of the image to the compressed only version. It can be useful to save memory if you know that the image content will probably not be used anymore for some time,
img configure "disk_shrink"
will force all compressed tiles to a temporary file.
I will end this introduction to in memory packed images through explaining configuration options.
var Link:ImagePrototype img :> new ImagePacked
that forces the tiles size to 64 x 64 pixels instead of being automatically computed.
img setup (image_prototype 0 0 10 10 256 256 color_gamut:"rgb") "clear_cache_size "+(string 4*2^20)
would force the memory used by clear tiles to 4 MB instead of the default, and:
img setup (image_prototype 0 0 10 10 256 256 color_gamut:"rgb") "packed_cache_size "+(string 16*2^20)
would force the packed tiles to use no more than 16 MB or be swapped out to a temporary file.
We have seen that Pliant image library supports two kind of in memory images: uncompressed and PACK4 compressed ones.
ImageConvert: changing the color model
There is an alternate way to achive the same, but relies on the generic 'setup' method:
var Link:ImagePrototype c :> new ImageConvert
Please notice that when 'setup' generic method is used as opposed to 'bind', then 'c' variable can either have type Link:ImagePrototype or Link:ImageConvert. The same will apply to other filters bellow
- add a fiew optimising options since some are absolutely necessary for some applications -
var ExtendedStatus s := aa setup img "antialiasing 4 4"
There are several techniques for providing proper (anti-aliased) vector drawing, text rendering, image rescaling.
Anti-aliasing code assumes (and checks) that the color model is 8 bits per component.
var ExtendedStatus s := sharp setup img "intensity 0.2"
Sharpening level is ranging from 1 to -1 (not exactly one since more that one is still possible). 0 means neutral and negative value means blur.
The sharpening algorithm is very naive, and just relies on a 3x3 matrix with the following coefficients:
On the other hand, implementation is not that naive since it does dithering in order to avoid quantization artifacts.
Sharpening code assumes (and checks) that the color model is 8 bits per component.
This is superseded by ImageTransfrom.
var ExtendedStatus s := r setup img "area 10 10 30 30 size 100 100 translate 5 5"
The first four parameters of 'bind' method specify the coordinates of the new images borders in millimeters. Then the two next parameters specify how many columns and rows the new image will have.
Let's take a few examples:
var Link:ImagePrototype img :> new ImagePixmap
The initial 'img' image is 100 x 100 mm, and contains 400 x 400 pixels.
On the other hand, if we write:
var ExtendedStatus s := r bind img 0 0 20 20 100 100 10 10
then the pixels in the 'r' image will be the same as in the previous sample, but the coordinates in millimeters of the new image top left corner will be (0,0).
This is superseded by ImageTransform.
Possible rotations are 'rotate_left' 'rotate_right' and 'rotate_180',
Reading the code shows that it's just piece of crap since it uses 'pixel' method instead of 'rectangle_read_map' that make is not only non general, but also poorly optimized.
Here is the more general version of ImageResampling and ImageRotate:
There is no 'setup' alternative.
The transformed image 't' will be a subpart (same resolution, but columns and rows removed) of 'p' just big enough to receive all pixels from the underlying image 'img' after applying the transformation.
In the transformation matrix, an angle of pi/2 means the image will be rotated 90° clockwise (because Pliant assumes that the zero coordinates are top left).
var Transform2 m := transform 0 0 1 1 pi/2 pi/2
the two first parameters of 'transform' are the translation, the two next the scale, and the two last the rotation. The scale and rotation are applied before the translation.
When reading the transformed image, some pixels of the transformed image can be mapped to some non existing pixels (outside of the boundaries) in the underlying image. It is safe to read these pixels, and the result will be zero filled pixels.
var Int x y count
on return x might be increased, and count might be decreased so that pixels ranging from updated x to updated x+count-1 are granted to be pointing existing pixels in the underlying image.
var Curve c
var ExtendedStatus s := l setup img "exposure 0.02 exposure0 -0.01"
The first version is the most general. For each dimension of the underlying color model, the corresponding lookup table is applied. Each lookup table contains 256 elements and the target value is expected to be a floating point value in the 0 to 255 range.
With the second version, the same lut will be computed for each dimension, according to the single provided curve.
With the third version, lookup tables will be provided according to a set of instructions.
Lut code is smart since it will do dithering in order to avoid quantization artifacts, except if 'setup' is used to do the configuration and 'fast' is provided in the set of instructions..
var Stream s
var ExtendedStatus s := l setup (null map ImagePrototype) "file [dq]file:/tmp/test.png[dq]"
An image read filter will be attached to the image, so that reading the lines from the image, as if it was already loaded, will transparently load the lines from the disk.
The string provided as the last parameter can be used to provide parameters to the read filter. See bellow for details about the set of options supported by each read filter.
var ExtendedStatus s := l bind s "filter [dq].png[dq]"
There is one huge constrain about lazy images: pixels must be read in the lines ascending orders.
var ExtendedStatus s := l bind "file:/tmp/test.png" "backward 4"
Then, since the 'read' method on an image does not return any status value, after reading one line from the image, testing if reading pixels from the on disk encoded image succeeded will be done through.
if (l query "status")="success"
The 'd' variable is containing a vector drawing. Vector drawing is the second layer of Pliant graphic stack.
Ripping means turning the drawing to an image. The resulting image is generally not built in one time, but rather tile by tile.
If the vector drawing uses a color model with one or more alpha channels, 'drop_transparency' option can be used to discard them on the final image:
var ExtendedStatus s := l bind l "drop_transparency"
Ripping can be a very complex and time consuming process, so there are a few tuning options available that might worth investigate for your specific application:
First, we can rip several tiles in parallel, one per available processor core:
var ExtendedStatus s := l bind l "burst"
Or we can force the number of tiles to be ripped in parallel to let's say 3 through:
var ExtendedStatus s := l bind l "burst 3"
But the best is generaly to use 'balance' option that will adjust automatically according to the load of the Pliant process.
var ExtendedStatus s := l bind l "burst balance"
Then, we can adjust the size of a ripping tile through 'cache' option that specifies the amount of memory to assign to each tile. The default is 4 MB per tile, and performances can often be significantly improved through increasing it, provided the application still gets enough memory available for the Pliant global cache.
var ExtendedStatus s := l bind l "cache "+(string 64*2^20)
The size of each tile can be further more reduced through 'step' option, also I don't remember anymore what it can be useful for:
var ExtendedStatus s := l bind l "step 512"
Finally, there is also a packed option that requests tiles to be in memory packed images. It might be used to render all the image at once. In the following sample, 'cache' option is used to disable the tile size limit so get a single tile in the end, and 'packed_cache_size' is an ImagePacked parameter that will force the tiles of the packed image (your head might start smoking at this point) to swap to a temporary file if the compressed image grows beyond 128 MB. Just don't use 'packed' option if you don't know exactly what to do:
var ExtendedStatus s := l bind l "packed cache ? packed_cache_size "+(string 128*2^20)
About the code, 'do_rip' is the function truly ripping one of the tiles, and 'access' is the function that will start extra ripping threads when burst mode is used.
ImageClip is intended to provide write masked access to an underlying image.
The first first four parameters specify a clip box (unit is millimeter). So, when writing to the underlying image through the filter, I mean when issuing 'write' to 'c' filtering image, no pixel of the underlying image will be modified outside the clip box.
VERY IMPORTANT: The 'mask' image does not behave as an alpha channel.
Let's take an example.
Clipping code assumes (and checks) that the color model is 8 bits per component.
The underlying image 'img' must use a color model with no alpha channel, and the 't' image will have multiple alpha channels (one per component in the underlying image).
When writing to 't' image, alpha channels informations will be applied.
Back to 'ImageClip' strange behavior, if you write:
Link:ImageTransparency t :> new ImageTransparency
then the behavior of 'c' is now the expected one. Now 'mask' behaves as an alpha channel on 'img', but this is handled at 't' level, not at 'c' level
Transparency code assumes (and checks) that the color model is 8 bits per component.
The module implementing file filters is /pliant/graphic/image/io.pli
First method: standard loading or storing
var ExtendedStatus s := img save "file:/tmp/test.jpeg" ""
When loading, filter option can be used to force the file filter selection otherwise based on the file name:
var ExtendedStatus s := img load "file:/tmp/test.img" "filter [dq].png[dq]"
When saving, filter option is available, but also the burst and balance options. This will read lines from the image in parallel. This can speed up things significantly on a multicore processor in case the image would be a color conversion or anti-aliasing filter:
var ExtendedStatus s := img save "file:/tmp/test.jpeg" "burst balance"
And finally, when saving, continue_flag option can be used to provide remote control. Basically, the flag will be checked for true before writing each line, so if another thread changes it, writing the image will stop in the middle.
var CBool flag := true
Second method: using a lazy image as described previously in this document
Third method: Low level interface
var Link:ImageWriteFilter f :> image_write_filter ".png"
Please notice that in the low level method, the stream must be an object, not just a local variable.
Fouth method: fast loading and saving pack4 encoded images
See in memory compressed images paragraph earlier in this document.
Here is now the description of special properties of each image reading and writing filter:
ppm and pgm file format
The simplest well know file format for encoding an RGB image.
As a result, when reading the resolution will have to be provided, or 72 dpi will be assumed:
var ExtendedStatus s := img load "file:/tmp/test.ppm" "resolution 300"
or, if the horizontal resolution is 300 dpi but the vertical resolution is 600 dpi:
var ExtendedStatus s := img load "file:/tmp/test.ppm" "resolution 300 600"
A 'negative' option is also supported by the read an write filters, than reverse (apply 255-x transformation) each byte value:
var ExtendedStatus s := img load "file:/tmp/test.ppm" "negative"
var ExtendedStatus s := img save "file:/tmp/test.ppm" "negative"
png file format
PNG is the recommended file format for lossless images.
Only resolution option is supported when reading (see example in ppm and pgm filter), and it will be used only when the resolution is not provided in the png file (resolution is an optional tag in PNG file format).
jpeg file format
This is not native Pliant implementation but rather in interface to libjpeg.
The very important option when writing a JPEG image is the quality. A quality of 1 would mean perfect image, but makes since the format is not optimized for that, and a quality of 0.25 writes an ugly image. Make your own tests (I personally consider 0.5 as low quality, and 0.9 as high quality).
var ExtendedStatus s := img save "file:/tmp/test.jpeg" "quality 0.9"
Resolution option is supported when reading (see example in ppm and pgm filter), and it will be used only when the resolution is not provided in the png file (resolution is an optional tag in JPEG file format).
Then, JPEG format supports comments, so you can add a comment at write tume through:
var ExtendedStatus s := img save "file:/tmp/test.jpeg" "comment [dq]Hello[dq]"
Reading the comment is ... not possible. Who is the stupid writer of this filter ?
tiff file format
TIFF file format is mostly used by professional softwares for encoding CMYK images.
The main TIFF option is the one specifying the compression to use. Supported value are 'packbits' 'lzw' and 'zlib'. 'packbits' is the faster one, 'zlib' the more efficient, but 'zlib' is an extension not supported by old readers. I have not mapped other compression methods such as the various CCITT since they have been designed for monochrome (1 bit per pixel) images and Pliant image library is not designed to handle this kind of images.
var ExtendedStatus s := img save "file:/tmp/test.tif" "packbits"
Resolution option is supported when reading (see example in ppm and pgm filter), and it will be used only when the resolution is not provided in the TIFF file (resolution is an optional tag in TIFF file format).
Maybe I should add code to automatically turn 1 bit per pixel images to 8 bits per pixel when reading, and enable writing 1 bit per pixel images either through transparent thresholding or dithering.
packed Pliant file format
This is the preferred file format for storing very high resolution (more than 600 dpi) text+image documents.
Supported options are 'plan' that will store the image as a set of plans instead of as a set of pixels, and it will improve compression in some situations, at the expense of speed, and 'tile_x' and 'tile_y' that force writing tiles instead of lines (don't use tiling options unless you know what you do):
var ExtendedStatus s := img save "file:/tmp/test.packed" "plan tile_x 512 tile_y 128"
Please notice that the on disk packed file format is currently an ASCII (UTF8 in facts) header, followed by an empty line, and then binary datas, and it should be changed to be PML encoded.
As a summary of all supported file formats, if you want to store RGB images on disk, you should either select PNG or JPEG depending if you expect lossless or lossy compression, unless the resolution is very high.
We can see the UI client as a five layer software:
So, the console drivers could be presented as another layer in a different document, but since it mostly deals with images and I don't plan to provide a lot of details since it's only used as the hardware glue code for Pliant UI client, I decided to include it here.
Console drivers prototype is defined in /pliant/graphic/console/prototype.pli
The only two functions for setting the screen content are:
method c paint img tx ty
method c copy x0 y0 x1 y1 xx yy
It means that the UI is really using the Pliant graphic stack to render the content on any platform, then call 'paint' to transfer the result on the screen, so that the viewed document will be pixel to pixel the same on all platforms.
Then, there is a single function for receiving mouse and keyboard events:
method c event key buttons x_or_x0 y_or_y0 x1 y1 options -> event
on return, event value will be 'character' or 'uncharacter' or 'press' or 'release' or 'move' (or something else and rare that I forgot to document).
Console drivers currently supported are:
The HTTP proxy is not a console driver since it does not provide the methods described in this paragraph, but it also plays the role of a possible UI fontend.
VNC RFB protocol is working very nicely as a Pliant console driver because (as opposed to Linux framebuffer) they very well designed the instructions set, which is ... the same as Pliant console with only 'paint' and 'copy' but with several compression mechanisms to help reduce the traffic between the client and the server (1).
The Linux kernel framebuffer frontend works poorly (unless and even using a patched tree) because Linux kernel framebuffer is very poorly maintained (2). No high end Linux guy is using it since they all use X11, so that the framebuffer is completely changing every few months (for better consistency as they say), but each of the rewrites stops as soon as X11 works nicely on top of it and never end as a properly working framebuffer.
The conclusion is: there is not much freedom when designing a generic console layer, so all people that spend some time experimenting end to basically the same. Please Linux framebuffer ... join us and finish you part of the work, which is hardware drivers.
I don't plan to provide great details about each console driver implementation since most of them are fairly simple glue code and they are unlikely to be used as anything else than Pliant UI client front-end.
Let's start by what a printer interface SHOULD be.
A printer should be defined by:
Then sending a document to the printer should be sending a PACK4 encoded bitmap with the right number of 8 bits components and at one of the supported resolution. No more.
The job of the printer would be to do anti-aliasing, splitting inks to cartridges (as an example when there is both a cyan and light cyan cartridge) and dithering (3).
This model works great for both low end printers and very high end one that print many pages per minute.
But for a high end printer with a RISC processor, Postscript is better since it frees the PC from calculations ?
No. For three reasons:
So, while introducing the various printer drivers supported by Pliant, I will comment on how far they are from the optimal model just described.
At application level, driving a printer is the same as writing files on disk. So the four methods described in 'On disk file formats' paragraph apply also to printers.
var ExtendedStatus s := img save "device:/usb/lp0" "filter [dq].escp2[dq] model [dq]Epson R800[dq]"
'device:/usb/lp0' does not work under Windows because under Windows an USB connected printer is not accessible directly, as opposed to a parallel port connected printer.
LPR protocol specifications published in RFC1179, 18 years ago, specifies that the size of the data file is provided at the beginning of the content. This is not convenient since it prevents to send the file on the fly (because in such a situation the size of the file is not known soon enough). As a result, RFC1179 states that zero shall be provided and it means that data file end will be TCP connection termination.
Let's now list and comment various provided printer drivers:
This driver supports most Epson inkjet printers.
The problem with ESCP2 language is that it's too low level. You have to provide one or two bits per cartridge per pixel. It means that dithering has to be performed on the computer side, and it results in a too big file.
The other problem is that, also Epson inkjet printers technologies mostly stopped significantly evolving years ago (basically, with the 3 pl ink pigmented printers generation, I mean Stylus photo 2000, 4000, 7600, 9600), they still tend to change some details with each model (beyond changing nozels number and spacing and extending cartridge selection instruction) that makes upgrading the driver mandatory ... for nothing (except maybe have the RIP sellers make more money through upgrades).
They also seem to have conflict between handling features (like the cutter) at ESCP2 instructions set level, or at the no use job-ticket EJL layer they added on top of it at some point.
Back to concrete, when using the Pliant ESCP2 driver, you have to provide 'model' option is in the example above, but it is likely that your model be not supported, so you end through selecting a not too different one, than provide extra options to force some other settings.
Anyway, here is a subset of the options you can use with this driver:
var ExtendedStatus s := img save "device:/usb/lp0" "filter [dq].escp2[dq] model [dq]Epson R800[dq] offset 5 5"
Then ... there is no then at the moment.
About Epson driver implementation, if you read the code, you will see that the complex part is dithering because having two cartridges with the same color (cyan and light cyan) and three possible dot sizes on each cartridge makes dithering much more complex than using a single cartridge and a single dot size.
The general idea of Pliant one bit dithering (as opposed to the one used for dealing with quantification issues and used by several image filters such as ImageLut introduced earlier in this document) is just to use a very large thresholding matrix.
Back to dithering implementation in ESCP2 driver, among the 6 dot levels available (for the cyan as an example, we have 3 of them using the dark cyan cartridge, and 3 of them using the light cyan) we use only 3 of them. The general reason for using few is that the more levels you use, the more likely you are to fall on not smooth enough transition issues. The three levels we use are a big dot on the dark cartridge, or the smallest one on the dark cartridge, or the smallest one on the light cartridge. Deciding among the 3 dot levels is performed in the code block marked with the comment 'handle pixel x y on head h , which level is l' in the commented out code block marked as 'unoptimized version'.
Setting ESCP2 driver ink adjustment parameters escp2_density, escp2_middle and others is intended to provide as much ink as possible at 100%, and a flat density curve (the density curve is drawn from computing the effective density from the output of the spectrocolorimeter), within the limit of having not too much ink at 100% on all colors. It can be tricky if the relative size of various dot levels and pigments proportion between the light and dark cartridge are not the expected ones as a result of a model change. In such a case, it might require adjusting transition parameters such as escp2_light_removal_start, escp2_light_removal_power and others, so ... it's outside the scope of this initial documentation. Too much is too much.
The other operation that the driver has to perform on a desktop Epson inkjet printer (as opposed to professional models that do it on printer side) is what they call weaving.
Another important setting to think about is 'unidirectional' option. If you set it, the head will print only from left to right, so printing will be slower because the time to move head back to the left will be lost, but head positioning will be more precise, so the final result will be sharper.
Provides PCL5 and PCL6 support.
This is the preferred language for most laser printers.
PLC is a vector drawing language, but Pliant driver will send an image instead. See 'theory' subparagraph for justification.
Just like ESCP2, PCL has a PJL jobticket language on top of it, that just like with ESCP2 worth nothing but troubles because most printer will want something special at PJL level or just not work ... even if PCL5 or PCL6 compatibility is claimed on the advertising papers.
The good news about PLC5 is that it provides a compression mechanism named 'Seedrow' that is a very decent one (packbits like working on pixels instead of bytes, and with a same as on the previous line extension, that makes it not too far from Pliant PACK4 capabilities).
Now the bad news are:
About implementation, the code is a bit simpler than with ESCP2 because no complex dithering is required. Anyway, providing 'pcl_dither' option switches the driver to use 1 bit per component instead of 8, so use Pliant matrix thresholding based dithering.
IJS is a standard interface introduced by HP to provide the document to print as an image to a server (an executable program in facts) that is providing drivers for many printers.
IJS interface is very much like Pliant one, so Pliant glue driver is fairly short.
IJS is expecting at least one 'model' option to decide the IJS driver to use:
var ExtendedStatus s := img save "device:/usb/lp0" "filter [dq].ijs[dq] model [dq]hp color LaserJet[dq]"
See IJS documentation for the list of models available in your IJS server.
If the IJS software to use is not 'hpijs', it can be changed using 'ijs_server' option.
Roughly speaking, IJS is designed for desktop printing on (mostly HP) low end inkjet printers that have so few computing power that they need the PC to do dithering (as opposed to printers using PCL5 or PCL6).
Gimpprint is the first free library that tried to provide high quality printing on inkjet printers.
I have to thank Gimpprint guy Robert L Krawitz for documenting Epson printers at a time Epson was not providing detailed specifications.
Anyway, Gimpprint is using too smart dithering algorithms that end in just disturbing color management, and still very primitive color calibration techniques.
Gimpprint also use a 'model' option.
The Pliant gimpprint glue code should not be too hard to understand, except that they use plenty of callback functions instead of enabling the external software to provide them lines one after the other.
You might want to try Gimpprint driver if you need to drive a supported Canon inkjet printer.
If you want desktop printing, a PCL compatible printer driven using Pliant native PCL driver is probably the way to go, and if you want high fidelity photos, you need an Epson printer with pigmented inks, and a color profile for it (with the right paper), but building a color profile requires a spectrocolorimeter so is not what average Jo user can do.