Pliant graphical stack vector drawing layer
The vector drawing layer prototype is defined as 'DrawPrototype' data type in /pliant/graphic/draw/prototype.pli
As I have pointed out in introduction articles, no vector drawing instruction set can be complete, so Pliant design choice is to make it minimal, with a more powerful underlying image layer enabling to efficiently send missing vector instructions as images.
A drawing surface (or device if you prefer) has type 'DrawPrototype'.
Initializing the drawing surface is performed through 'setup' method:
method d setup proto options -> status
oarg_rw DrawPrototype d ; arg ImagePrototype proto ; arg Str options ; arg ExtendedStatus status
'proto' has type 'ImagePrototype' introduced in the image layer article, and provides informations about the boundaries of the drawing surface and the selected color model. 'options' is intended to provide driver specific options.
The main drawing instructions are:
method d image img t
oarg_rw DrawPrototype d ; oarg_rw ImagePrototype img ; arg Transform2 t
paste an image.
2D transform matrix are defined in /pliant/math/transform.pli and explained here.
method d fill curves mode t color
oarg_rw DrawPrototype d ; arg Array:Curve curves ; arg Int mode ; arg Transform2 t ; arg Address color
fill an outline with uniform color. The color are provided not as a value, but as the address of the color value.
Possible values for 'mode' are 'fill_envenodd', 'fill_nonzero' and the rarely used 'fill_union'.
Please notice that the transform matrix parameter is provided in order to enable implementing the 'text' drawing instruction below as a set of 'fill' instructions without modifying each curve encoding each glyph outline. See the code in /pliant/graphic/draw/prototype.pli to see how straightforward translating 'text' to 'fill' is.
method d text txt font kerning t color
oarg_rw DrawPrototype d ; arg Str32 txt ; arg Font font ; arg Address kerning ; arg Transform2 t ; arg Address color
draw some text.
'kerning' is either the 'null' address or the address of a buffer of Float32 values, one for each character in 'txt', that will provide characters sliding informations. Before drawing each character, the transform matrix 't' is translated by the font stepping vector multiplied by the value associated with the character in the 'kerning' buffer. What I call font stepping vector is the vector associated with the font as opposed to the one associated with each different glyph that is effectively used to move the cursor after drawing each glyph. In a fixed font, they are all equal.
The last two important vector drawing operations are providing advanced clipping capabilities:
method d clip_open x0 y0 x1 y1 -> dc
oarg_rw DrawPrototype d ; arg Float x0 y0 x1 y1 ; arg Link:DrawPrototype dc
method d clip_close
oarg_rw DrawPrototype d
When you call 'clip_open', you receive a new clipping drawing surface. Starting from that point, you can send each drawing instruction either to the main drawing surface, or to the new clipping surface. The clipping surface is just an alpha channel added on top of the main drawing surface. When you send drawing instructions to the clipping surface, you are adjusting the alpha channel value. When you send drawing instructions to the main drawing surface, the changes applied are filtered on the fly by the alpha channel. The color model of the clipping surface is 'grey' (a single 8 bits channel per pixel) whatever the color model of the main drawing surface might be.
'x0' 'y0' 'x1' 'y1' can either be all 'undefined' or they will define a first clipping rectangle, so that any drawing outside the provided rectangle boundaries will be ignore.
'clip_close' drops the clipping surface, and restores unfiltered access to the main drawing surface.
As a summary, 'text' instruction is a specialized version of 'fill', which is a specialized version of 'image'.
See 'More on drawing API' at the end of this document for exhaustive description of the drawing API. Anyway, extra methods in the 'More on drawing API' section are not drawing instructions (except 'rectangle' which is of no use).
Let's see the real code implementing the three main vector drawing instructions. I mean 'image' 'fill' and 'text'.
First, 'image' vector drawing instruction (pasting an image on the drawing surface positioned using a 2D transform matrix). The effective code is implemented half in /pliant/graphic/image/transform.pli and half in /pliant/grahic/draw/image.pli.
The code in Pliant /graphic/image/transform.pli provides the image content at the resolution of the drawing surface underlying image, with the 2D transform applied.
Then the code in 'image' method (in /pliant/graphic/draw/image.pli) copies the pixels. For each line in the drawing surface underlying image (it is 'd:image' in the code), it copies the pixels from the transformed image (it is 'final' in the code) to the drawing surface underlying image.
The code in 'image' method (in /pliant/graphic/draw/image.pli) is also converting pixels from the image to past color model to the drawing surface color model, either through binding an 'ImageConvert' filtering image on the image to past, or through directly calling the huge 'convert' method implemented in /pliant/graphic/color/gamut.pli that does the effective conversion work.
If you read the code of 'image' method in /pliant/graphic/draw/image.pli, you will notice that it looks like this:
if source_transparency and final:gamut:transparency=1 and d:image:gamut:transparency=0
# ALGO 1
# ALGO 2
ALGO 2 is the standard one.
ALGO 1 is a naive algorithm that will be selected if the image has an alpha channel, but the underlying surface does not. It will copy only pixels with 128 or more on the alpha channel. So, ALGO 1 will be applied if no proper transparency policy has been defined. See 'Advanced transparency' section bellow for extra explanations about how Pliant graphic stack handles transparency.
Second, 'fill' (filling with a flat color an area of the drawing surface defined by a set of bezier or equivalent curves) is implemented in module /pliant/graphic/vector/outline.pli:
The main function is implemented near the end of the module:
method img fill curves mode t color
oarg_rw ImagePrototype img ; arg Array:Curve curves ; arg Int mode ; arg Transform2 t ; arg Address color
Each curve will be converted to a polyline though calling 'polyline' method ('polyline' method is implemented in /pliant/math/curve.pli), then the polyline will be converted to a set of descending chains through calling 'outline_build_chains'. A descending chain is a set of points with increasing 'y' values. All descending chains are stored in a data with type 'Outline'. Lastly, 'fill' method with the 'Outline' data as a parameter is called (implemented near line 125), and for each line in the image, it sorts the descending chains according to the 'x' value of the intersection of the descending chain with the current line, then draws horizontal segments.
Once again, I don't know if this algorithm is already well known in scientific literature or new.
Third, the implementation of 'text' drawing instruction; we'll see it at the end of the next section introducing fonts.
As a conclusion 'DrawImage' type defined in /pliant/graphic/draw/image.pli is the point where vector drawing instructions will be connected to effective software implemented (as opposed to hardware accelerated) drawing algorithms; in other words, where vector drawing instructions will be truely executed.
Pliant natively supports:
PostScript Type 1 fonts (file extension is .pfb)
Spline Font Database fonts (file extension is .sfd)
You can easily use any True type font (file extension .ttf) through converting it to .sfd using 'Font forge' free software. Just load the font, then safe it.
Fonts support is implemented in /pliant/graphic/vector/font.pli. PostScript Type 1 font decoding is implemented in 'load_postscript' method, and Spline Font Database font decoding is implemented in 'load_sfd' function. The function to map the font name to a file name, to handle the Pliant global cache interface, and to call 'load_postscript' or 'load_sfd' is the 'font' function at the very end of the module:
var Link:Font f :> font "bitstreamvera/Vera"
Here are a few informations contained in 'Font' structure:
console f:bbox_x0 " " f:bbox_y0 " " f:bbox_x1 " " f:bbo_y1 eol # The bounding box of a standard character in the font
console f:vector:x " " f:vector:y eol # the font standard stepping vector
Then come various functions to query and use fonts:
var Vector2 v := f vector "Some text" null
Returns the vector that drawing the font with neutral transform matrix would move the draw cursor (there is no effective draw cursor in Pliant, this is just a way of speaking). The first parameter can be either 'Str32' or 'Str'.
Be careful that if it's 'Str' then the string is not assumed to be just UTF8 encoded, as it is the standard in Pliant, but is assumed to contain only ASCII characters, so if your text is UTF8 with non ASCII characters, you should explicitly cast it to 'Str32'.
The second parameter is either 'null', or the address of the kerning buffer to specify characters sliding (see 'text' instruction at the beginning of this document).
Since most fonts move the cursor to the right, you may want to get only the length:
var Float l := f length "Some text" null
You can also get the bounding box:
f bbox "Some text" null (var Float x0) (var Float y0) (var Float x1) (var Float y1)
There are several variants of 'bbox', but the most powerfull is:
method f bbox text kerning flags scale x0 y0 x1 y1
arg Font f ; arg Str32 text ; arg Address kerning ; arg Int flags ; arg Float scale ; arg_w Float x0 y0 x1 y1
The extra parameters are 'flags' and 'scale'.
'flags' can be 0, or 'bbox_l', or 'bbox_h' or 'bbox_l+bbox_h'.
'bbox_l' means that we don't want to get how much the cursor is moving forward as a result of drawing the string, but rather the effective length of the drawing, so the empty space before the first character and after the last one will not be counted.
'bbox_h' means that we don't want to get the all standard height of the font, but the real height of the characters in the string.
Let's say it clearly: there are too much variants of 'vector' 'length' and 'bbox', so it's bad design in the end.
Last part in the /pliant/graphic/vector/font.pli module is implementing effective drawing in an image. Once again, there are too many variants of the same function. Here is the good one:
method img text txt f kerning t color speedup
oarg_rw ImagePrototype img ; arg Str32 txt ; arg Font f ; arg Address kerning ; arg Transform2 t ; arg Address color ; arg Int speedup
The only strange parameter is 'speedup'.
If speedup is zero, we do normal vector drawing through calling 'fill' vector drawing instruction for each character.
If speedup is one, and the font is small, 'rasterize1' might be called to turn the glyph to a set of horizontal segments, so that it will be drawn by 'rcharacter1' that will call 'fill' (this is 'fill' instruction of the image layer; don't confuse with 'fill' instruction of the vector drawing layer) to draw each segment.
If speedup is two, and the font small, 'rasterize2' might be called to turn the glyph to a set of horizontal segments, with each pixel in the segment being assigned an alpha channel level. In other words, 'rasterize2' draws characters with anti-aliasing.
Please notice that speedup 2 is not always better than speedup 1. If you plan to store the result of the drawing in a PACK4 encoded image (see image layer), then having higher resolution and speedup 1 will bring better result. Same apply if you draw in an image that will be later anti aliased to bring optimal quality.
As an example, in the Pliant UI, in normal fast rendering mode, speedup 2 is used, but if you press right 'Ctrl' key or click on the mouse right button to get optimal rendering, speedup 1 followed by x4 anti-aliasing will be used.
Module /pliant/graphic/vector/freetype.pli provides the following function:
var Link:Font f :> font_freetype "strange font file name" "face 0"
The font is loaded through interfacing libfreetype. The second parameter enables to select the requested face if the font file contains several faces.
Please notice that 'font' implements caching, but 'font_freetype' does not, so if you use the second, you have to do caching at application level, or the font will be decoded many times and might also consume several times more memory.
Once again, the design choice of Pliant vector drawing layer is to provide very few drawing instructions in the API.
Turning a line stroking vector drawing instruction to a 'fill' vector drawing instruction is implemented in /pliant/graphic/vector/stroke.pli
In the code:
'corner' function adds one or two points to 'line' curve to draw one side of the corner near 'p1'.
'segment' function is drawing the 'p0' to 'p1' segment, plus the corner at 'p1', so it's transforming the segment in two curves (one for the segment, one for the corner).
'conservative_stroke' will stroke the line through filling each segment one after the other.
'stroke' will try to build the single outline that draws the line, through converting the line to a polyline then calling 'corner' for each point of the polyline, and then once again for each point on the way back to draw the other side of the line. If one call to 'corner' fails because the segments of the polyline are wide and short instead of being long and thin, then it will fallback to 'conservative_stroke'.
On the design point of view, it makes sense not to provide a line stroking function in the base vector drawing API because either stroking is intended to be high quality, and then turning it to 'fill' is ok, or stroking has to be fast and hardware accelerated like in a CAD software, but then the subtle 'cap' 'join' 'miter' of the PostScript model can be completely ignored. Moreover, most hardware will not accept the line as a sequence of Bezier curves, but require the main processor to turn it to a polyline. As a result, in the end, the function that is interesting to implement to benefit from hardware acceleration is just the one drawing a segment (or a polyline in order to avoid providing each corner position twice):
method d segment x0 y0 x1 y1 width color
oarg_rw DrawPrototype d ; arg Float x0 y0 x1 y1 width ; arg Address color
# This function does not exist in Pliant graphic stack
Turning an axial or radial shading vector drawing instruction to an image is implemented in module /pliant/graphic/vector/shading.pli
In the code, 'shading_color' is computing the color 'c' at position 'f', where 'f' range is 0 to 1, from an array 'cs' of colors provided as part of the shading instruction parameters. In the previous sentence, position is a fuzzy notion defining the relative distance of the current point from the shading implicit limit curves.
Then 'axial_shading' function is a shading defined by two points, so it's the same as shading between two parallel lines (the lines are the two lines passing by each provided point and orthogonal to the line connecting the two provided points).
'radial_shading' function is shading defined by two circles, so it's a generalization of circular shading defined by a single circle. See PDF specifications for extra details.
There are three weak points in Pliant shading implementation:
Proper shading requires to use some dithering mechanism to avoid quantization artifacts.
Color outside the limits is not properly defined so not granted to be consistent with PDF specifications.
It's slow because we compute the exact formula for each pixel instead of computing the position and tangents for each small square area.
As a conclusion, Pliant current shading implementation is ... toy.
Now, on the design point of view, what justify not including shading instructions as part of the base vector drawing API is the lack of generality. One serious candidate would be to define shading with two curves, and two colors. Both axial and radial shading are a particular case of this more general case:
method d shading curve0 color0 curve1 color1
oarg_rw DrawPrototype d ; arg Curve curve0 curve1 ; arg Address color0 color1
# This function does not exist in Pliant graphic stack
Another way would be to adopt the elementary point of view. A stretched out rectangle defined by the position and color of it's four corners (or a triangle since we can define a rectangle as two triangles). This might be a better foundation for images that come from 3D word and for hardware acceleration:
method d shading x0 y0 c0 x1 y1 c1 x2 y2 c2 x3 y3 c3
oarg_rw DrawPrototype d ; arg Float x0 y0 x1 y1 x2 y2 x3 y3 ; arg Address c0 c1 c2 c3
# This function does not exist in Pliant graphic stack
As you can see, the 'best' solution is not obvious, so not choosing makes sense.
A good sample for demonstrating Pliant graphic stack vector layer is the tiny RIP provided in /pliant/graphic/sample/rip.pli
We have seen earlier in the section 'Drawing algorithm' that the data type responsible for turning vector drawing instructions to pixels is 'DrawImage' type, defined in /pliant/graphic/draw/image.pli and is implementing the vector drawing API ('image' 'fill' 'text').
An 'ImageRIP' type, defined in /pliant/graphic/image/rip.pli is often used on top of it. This one is implementing the Pliant stack image API (pixels read). 'ImageRIP' data type will lazily create tiles images (with type 'ImagePixmap' or 'ImagePacked'), and bind a 'DrawImage' data type to the tile to perform the rendering.
Here is a schema illustrating how the all machinery is working in such a situation:
The PDF reader parses the file, and records drawing instructions in a display list ('load' instruction near the beginning of the 'rip' function in /pliant/graphic/sample/rip.pli)
The output file driver requests the lines to the ImageAntiAliasing image filter ('save' instruction near the end of the 'rip' function)
The ImageAntiAliasing filter requests pixels from the ImageConvert image filter
The ImageConvert filter request the pixels from ImageRIP
When the requested pixels are not in an already ripped tile, ImageRIP creates the ImagePixmap image tile, then creates a DrawImage drawing surface, connects it to the ImagePixmap image tile and the DrawDisplayList vector drawing instructions set, and finally calls 'play' on the display-list to render the tile ('do_rip' function in /pliant/graphic/image/rip.pli)
When DrawImage is bind to ImagePixmap, it creates an ImageTransparency image filter and puts it in the middle to apply the transparency rules ('setup' function in /pliant/graphic/draw/image.pli).
In the end, the effective process is:
Drawing instructions are received from the display-list and rendered (turned to image 'write' instructions) by DrawImage
Transparency is applyed on the the fly by ImageTransparency when DrawImage sends 'write' instructions to the underlying tile image
Then the color model switch and anti-aliasing is applied on the fly when pixels are pulled from ImageRIP tile to be written to the PNG output image.
From this schema, we can also learn limits/compromise in ripping technology.
If the drawing in the PDF file is too complicated, the display-list will grow until it exhausts memory. So, it would be nice to be able to not store it, so render all the drawing in a single pass instead of tile by tile. Now, the problem is that if the final image is large and high resolution, it might not fit in main memory also, so that tiling, so display list storing be required. One solution might be to use an in memory packed image, but then the compression, so the ability for it to fit in the main memory is also related to the drawing complexity. In the end, we understand that the performances of the RIP will be very much impacted by the size of tiles and fact they use compression (ImagePacked data type) or not (ImagePixmap). Even if the PDF is simple enough to generate a display-list that fits nicely in the main memory, on one end small tiles will provide more computing overhead because as an example 'fill' instructions will have to be decoded from Bezier curves to polylines more times, but on the other hand the tile pixels in the tile might better fit the processor 4MB or so cache.
We also see the limits of Pliant graphic stack: 'fill' instructions are stored in the display lists as Bezier curves or equivalent as opposed to polylines, so the conversion will be performed several times as soon as tiling is used. On the other hand, it makes the code easier because display-lists have sub-display-list notion, so that the same curve can be drawn several times with a different 2D transform matrix, so that it's optimal polyline approximation might not always be the same.
When the vector drawing layer is used in Pliant UI, the draw instructions are generated by the positioning layer, so the storage issue is imposed by the positioning layer and unrelated to the vector drawing layer, and a screen resolution is always small compared to printers one, so that drawing all at once is a fairly obvious choice (see 'draw' method in /pliant/graphic/ui/client/window.pli)
Here are various transparency models, sorted from the most limited one to the most powerful one:
PostScript level 2: a single 1 bit per pixel alpha channel that has to be set through a single 'fill' vector drawing instruction
Separated PostScript, PDF 1.3: multiple 1 bit per pixel alpha channels (one per ink). Each of them still has to be set by a single 'fill' vector drawing instruction
Pliant: multiple gray scale alpha channels that can be set using any set of drawing instructions
PDF 1.4: multiple gray scale alpha channels that can be set using any set of drawing instructions, plus instructions grouping and mixing operators.
In order to illustrate this, let's study a sample under Pliant transparency model.
On one of the pixels in the image, let's assume the value associated with the cyan channel is 20%, and the value on the associated alpha channel is 50%.
Then we execute a first 'fill' instruction with value 40%. The new value associated with the cyan channel will be 30% (40*50/100+20*(1-50/100)).
Then we execute a second time the same 'fill' instruction. The new value associated with the cyan channel will be 35% (40*50/100+30*(1-50/100)).
Now, with PDF 1.4, if both instructions are grouped, they will be first drawn in a copy of the image where the current alpha channels have been disabled.
So, on the copy image, the value associated with the cyan channel is 20% at the beginning,
then it's 40% after executing the first 'fill' instruction (disabled alpha channel),
and it's still 40% after executing the second fill instruction.
Then when the grouped instructions set ends, the new 40% is mixed with the old value 20% using the alpha channel, so the final result is now 30% (instead of 35% with Pliant transparency model).
PDF 1.4 also provides flexible final mixture operators, so if the old value is 20%, and the new one is 40% at the end of the grouped instructions set execution, you can specify that the result will be maximum of the two values, the minimum, the product, the invert product and so on.
The PDF 1.4 transparency model is not only more powerful than Pliant one, it's also more consistent. Let's assume that you have received an illustration, which is a complex drawing, and you want to past it on your page, but for strange aesthetic reasons, you want to make it half transparent. With PDF transparency, you just group, set 50% alpha channel, and here it is. Basically, you can handle the illustration has a single consistent object, so you can do exactly everything you could do if it was a single 'fill' or 'image' instruction.
Now the drawback is that when the graphic engine start to execute a grouped set of instructions, it has to make a copy of the underlying image, so when rendering a large tile to reduce the repetitive Bezier to polyline overhead seen earlier, the amount of memory consumed by the tile might be doubled, and even more because, for consistency reasons, nested grouping is not excluded.
Let's go back to Pliant graphic stack. All PDF 1.4 transparency model is supported in facts. It is not implemented in the general model explained at the beginning of this document, but it's added as a trick in display lists implementation. In module /pliant/graphic/draw/displaylist.pli, there is a 'flat_play' function that will do exactly what I have just described: make a copy of the drawing surface underlying image, execute the set of grouped instructions, and finally apply the mixture operator.
In the Pliant vector drawing layer, some filters exist that just modify drawing instructions on the fly and pass them to the next drawing surface, just like in the image layer some filters exist that just modify pixels on the fly and pass them to the next image.
'DrawConvert' is implemented in /pliant/graphic/draw/convert.pli and is changing the color model on the fly. So, 'DrawConvert' does the same in vector drawing layer that 'ImageConvert' does in image layer.
var Link:DrawPrototype d
var Link:DrawConvert c :> new DrawConvert
c bind d "gamut [dq]offset:cyan+magenta+yellow+black+p_485_c+transparencies[dq]"
See 'bind' method in the source code for possible other options.
'DrawTransform' is implemented in /pliant/graphic/draw/transform.pli and is applying a 2D transform on the fly:
var Link:DrawPrototype d
var Link:DrawTransfrom t :> new DrawConvert
t bind d "translate 100 60"
var Pointer:Transform2 t2 :> t transform # get a pointer to the 2D transform matrix
See the 'bind' method in the source code for possible options to define the 2D transform matrix.
For vector drawing layer, the interface for loading or saving to a file (a stream in facts) is very much the same as the standard API exposed at the beginning of this document. A file loading filter will generate drawing instructions, and a file writing filter will receive them.
The exact interface is defined in /pliant/graphic/vfilter/prototype.pli
So, a vector file format read filter will implement:
method f load stream options draw -> status
oarg_rw DrawReadFilter f ; arg_rw Stream stream ; arg Str options ; oarg_rw DrawPrototype draw ; arg ExtendedStatus status
and will provide drawing instructions to the 'draw' vector drawing surface passed as a parameter,
and a vector file format write filter will implement the API described at the beginning of this document ('image', 'fill' and 'text'), plus:
method f open stream options h -> status
oarg_rw DrawPrototype f ; arg_rw Stream stream ; arg Str options ; arg ImagePrototype h ; arg ExtendedStatus status
method f close -> status
oarg_rw DrawPrototype f ; arg ExtendedStatus status
in 'open' method, 'h' provides the boundaries of the drawing surface and the color model.
Please notice that some filters require to be able to do 'seek' in the stream (jump to another position instead of reading or writing sequentially), so 'draw_read_flags' and 'draw_write_flags' functions are provide to discover if the filter requires 'seek' capability on the stream.
Then the high level reading and writing functions are defined in module /pliant/graphic/vfilter/io.pli
var Link:DrawPrototype d
var ExtendedStatus s := d load "file:/tmp/test.pdf" ""
var Link:Stream s :> new Stream
s open "file:/tmp/test.pdf" in+safe
var ExtendedStatus s := d load s ""
var ExtendedStatus s := d save "file:/tmp/test.ps" ""
var Link:Stream s :> new Stream
s open "file:/tmp/test.pdf" out+safe
var ExtendedStatus s := d save s ""
Please notice that the 'save' instruction is assuming that the drawing surface is implementing 'play' method (see 'More on drawing API'), so that it's a kind of display list. As a result, in some cases, you might want to do it all by hand, so call 'draw_read_filter' to get the file format driver, then 'open', then use the filter as as standard drawing surface (send it 'image' 'fill' and 'text' calls), and finally call 'close'.
It's implemented in /pliant/graphic/vfilter/postcript.pli
Writing PostScript file implementation should be fairly easy to read, but you'll soon notice that it does not write a single color PostScript file, but writes separated PostScript.
Separated PostScript has been the trick used by printing industry around 1980-2000 years to cope with poor color handling in PostScript level 2. It consists in sending each plate content that will be used to print the document (each ink if you prefer) in a different page. All pages are using PostScript gray scale color model.
So, if you want to write color PostScript, just modify the code, and send me the patch.
Reading PostScript is implemented through calling GostScript free RIP to convert from PostScript to PDF, then using Pliant native PDF reader described just bellow. Once again, it's assuming that the file is separated PostScript, and use dirty ticks to find the ink associated with each page (see 'discover' part in the code).
Why do I provide so bad PostScript support compared to the remaining of Pliant graphic stack ?
About reading, I could have written a native PostScript reader in Pliant. I did it years ago in C++. Also I have not ported it to Pliant because PostScript is a language, and the side effect is that when a file does not work, you have to dig in the awful glue code most PostScript writers are putting in the file header, and this is terrifically time consuming. I could just trace the real operators in order to just ignore the glue, but the problem is often in the glue interpretation or that the glue code is testing it's environment in order to try to be smart (generate a PostScript file that auto adapts to the target device) and generates different real drawing instructions accordingly. So, in the end, debugging a non working PostScript file proved to be too tricky and time consuming.
About writing, PostScript is nowadays used only to drive PostScript (laser) printers since shared documents are always PDF. Then PostScript is not anymore a good language for driving a printer (See 'Printer drivers' 'Theory' part of the Pliant image layer article). So, the explanation about Pliant poor support might just be that it's not that useful anymore.
It's implemented in /pliant/graphic/vfilter/pdf.pli
If you start reading Pliant PDF reader code ... take an aspirin. It's a bulk nearly 3000 lines module.
Here are some guidelines:
The general context is stored in a 'PDFReader' variable. It contains a set of 'PDFContext' graphic state context because PDF, just like PostScript has a notion of push the graphic context value on the contexts stack, modify some parameters, execute some instructions, then restore the initial parameters through pulling the context from the contexts stack.
The 'load' instruction at the end of the module first reads the end the PDF to find the objects table offset, then reads it (see 'scan_reference' part). Then it loads the 'Pages' dictionary and is ready to enter the 'for (var Int page_num) ...' loop that will read each requested page.
Reading a page means set context parameters to default value, then find page bounding box, then fight like mad to find the right color model to use, and finally read the 'Contents' array and execute each object through calling 'process_instructions' function.
'process_instruction' function calls 'parse' function to find the next instruction to execute.
'parse' function calls 'token' instruction to read the next token from the file, and pushes on the stack all values it finds, until some instruction is found.
At that point, control get's back to 'process_instructions' that search it in the instructions dictionary, and runs it. Each PDF instruction has been recorded by a 'pdf_instruction' meta instruction at module compile time.
PDF uses a stack of typed object to pass parameters to instructions. The stack is implemented as the 'stack' field of the 'PDFReader' general context variable. Several helper functions are provided to make access easier, so code shorter:
'pick 0' returns the address of the top parameter on the stack. 'pick 1' returns the address of the next one, and so on.
'(pick 0) is Float' enables to test if the top parameter on the stack exists and is a valid floating point value,
and '(pick 0) as Float' casts the top parameter on the stack to a floating point value, returning the 'undefined' floating point value if the parameter was not a number or the stack is empty.
'pop 3' removes three parameters from the stack.
Lastly the really painful to read instruction is 'Do' because in PDF, it does several different things such as pasting an image (subtype="Image"), or calling an object that will behave as a subroutine (equivalent of 'gosub' instruction in Basic programming language) enabling to use the same sequence of drawing instructions several times (subtype="Form"). Please notice that the subroutine notion in PDF is not part of Pliant vector drawing API and rather supported through a hack in Pliant display list implemented as 'DisplayListInclude' data type.
Maybe I'll write a separate article about the Pliant PDF reader module at some point, but that's all for today. Feel free to drop me an email if you are interested to hack it and expect more help to dig in.
Writing PDF is ... not implemented. I really have to kick my ass, but I'm not supple enough to do that myself, and unless somebody writes the kick over the Internet RFC, I cannot implement it :-)
More seriously, the only issue with PDF writing is translating the 'text' instruction. One solution is to turn everything to 'fill', but many readers might crawl as a result. The other would be to define PDF embedded fonts on the fly, but I'm afraid about poor support in readers.
Now, about Pliant PDF reader quality.
Depending on the kind of document you are reading, it's not at all a good one because it can fail to properly read even simple desktop documents, or it's the best among free ones, because it implements the all PDF color rendering model (multiple alpha channels), so it's not competing with the free as a beer Acrobat, but with Acrobat pro. In other words, it can read documents for high end printing systems including Pantone (tm) spot colors and render properly related inks overlay.
Why no SVG support ?
This is an open question. PDF is the de facto standard. It's not a good standard because it's far too complex (1000 pages specifications to implement three 'image' 'fill' and 'text' operators is just a nonsense because of the raised interoperability issues level). So, the question is double: first, where does SVG cursor stands in the Pliant to PDF complexity scale ? Then, both PDF and Pliant use a rendering model that is perfectly suited for any kind of printing. Is SVG suited for printing, or still practically restricted to on screen rendering ?
I have just not studied SVG enough to provide clear answers.
The following section is for completeness. It is mostly intended to save you time if you start studying the code, but is probably a bit short for direct understanding. Feel free to ask for clarifications.
'DrawPrototype' implemented in /pliant/graphic/draw/prototype.pli exposes additional methods for advanced use:
method d rectangle x0 y0 x1 y1 color
oarg_rw DrawPrototype d ; arg Float x0 y0 x1 y1 ; arg Address color
this is a specialized version of 'fill'. I have doubts it be any useful in facts.
method d tag_open id
oarg_rw DrawPrototype d ; arg Str id
method d tag_attribute attr value
oarg_rw DrawPrototype d ; arg Str attr value
method d tag_close
oarg_rw DrawPrototype d
'tag_open' / 'tag_close' enable to provide a document as an XML like sequence (tags with attributes) with the drawing instructions concerning each tag provided between the 'tag_open' and 'tag_close' calls.
method d warning message
oarg_rw DrawPrototype d ; arg Str message
report some problem.
method d trouble_open -> dt
oarg_rw DrawPrototype d ; arg Link:DrawPrototype dt
method d trouble_close
oarg_rw DrawPrototype d
'trouble_open' creates a new drawing surface that can be used to show where the trouble is through drawing a red circle around the concerned area or something like that.
method d bind draw options -> status
oarg_rw DrawPrototype d draw ; arg Str options ; arg ExtendedStatus status
binds the drawing surface to another one. This is used to configure filtering drawing surfaces such as 'DrawTransform' that applies a 2D transform to all drawing instructions it receives.
method d image_prototype options -> proto
oarg DrawPrototype d ; arg Str options ; arg ImagePrototype proto
query the drawing surface about it's dimensions and color model. This is a bit the opposite of 'setup' method explained at the very beginning of this document.
method d backdrop options proto -> image
oarg_rw DrawPrototype d ; arg Str options ; arg_rw ImagePrototype proto ; arg_C ImagePrototype image
image :> null map ImagePrototype
query the drawing surface about the underlying pixmap images. Accessing the backdrops is used to implement PDF transparency extensions, but it's unlikely to be of any use for other applications.
method d play draw options
oarg_rw DrawPrototype d ; oarg_rw DrawPrototype draw ; arg Str options
some drawing surfaces, mainly display lists, can both receive drawing instructions (record), and generate them (play). This instruction requests to generate drawing instructions and send them to the provided 'draw' surface.
method d query command -> answer
oarg_rw DrawPrototype d ; arg Str command answer
method d configure command -> status
oarg_rw DrawPrototype d ; arg Str command ; arg ExtendedStatus status
'query' can be used to query some properties of the drawing surface, and 'configure' to change them. Here are some examples:
var Link:DrawDisplayList d
d configure "shrink" # asks the display-list to discard some internal objects it built in order to speedup operations
if ((d query "memory") parse (var Intn mem))
console "Estimated memory consumed by display the list is " mem\2^20 " MB" eol
d configure "reset" # clear all the display list content