ZjStream

 

Reference
     SuperPrint Core Exports
     SuperDriver
     Files and Stream Format
          SDD
          ZINSTALL and Zuninst
          Zeno.inf
          ZjStream
          IMF
          Threshold Arrays
     UI Component Integration
     zPJL

History and Overview

The Zj-RCA controller for the Minolta 3001 color laser printer was the first of its kind.  While other host-based printing systems with very simple engine interfaces have been built, the Zj-RCA controller was the first to solve the problem with a standard parallel interface, with high performance and while maintaining a low controller cost.  Other host-based printers have at least one significant drawback.  The Minolta 3001 Zj was the first truly practical host-based color printing system.

As with most projects, you learn more about it while it is in development.   The Zj-RCA development team needed some format for transferring data from the host to the printer.  Since there was one person working on the driver and one person on the firmware, this format could be any ad hoc temporary format.  The focus was to get a system that solved the technical goals for the project, and details could later be revised and polished.  The developers were a victim of their own success: the initial stream format was so good, that no other stream format was required until much later on, when some fundamental changes were necessary for the release of the commercial product.   The initial stream format is sometimes called the "old stream format."  The two are similar but not compatible.   The "old stream format" is an unreleased, internal, development-only version.  You should never need to know its details because it is dead, dead, dead.   It is worth mentioning because it was in use for a while and exposed to developers and testers.  The release stream format is what is in use now and this is what is documented here.

Basics

The SuperPrint Zj driver constructs a stream of bytes which represent a single print job, where a job consists of one or more pages.  Usually, the driver renders a single job into a single "Zj-stream," and copies (collated and uncollated) are cooperatively manufactured by the language monitor and the printer.  The Zj-stream sent to the Zj printer is formatted in a very structured and relatively simple way.

This document provides a description of the Zj-stream which in effect is the Page Description Language for a Zj-printer.  Using this document, the definitions listed in the INC\ZJRCA.H header file included in the DDK distribution, and an understanding of the JBIG image compression format, it is possible to create print jobs through means other than SuperPrint (from a MacOS system or an external Digital Front-End, for example).

It is desirable for the driver to optimize the Zj-stream for the target printer.   So, byte swapping may be performed on 16 and 32 bit values within the stream so that the controller always receives these values in its native format.  If the first 4 bytes of the stream are "ZJZJ," then the integer values are little-endian (Intel style).  If the first 4 bytes of the stream are "JZJZ," then the integer values are big-endian (Motorola style).  If the first 4 bytes are anything else, the Zj-stream is invalid.

Data Communication & Stream Syntax

The Zj-stream may be serially captured in a .ZJS file.  Parsing requires no "seek" facility (i.e., does not require random access to the stream).  Usually the bytes are passed from the driver to the language and port monitors and then through the communication interface to the printer.  While the Zj-stream format is logically independent of the data communication protocol, its structure was created to accommodate a simple but high-speed transfer.

Following the 4-byte signature described above, there are a series of byte streams called "chunks."  The entire Zj-stream is partitioned into these "chunks" (described in detail below).

4-byte signature

chunk 1
chunk 2
...
chunk n

Zj-Stream Top Level Organization

The following EBNF (Extended Backus-Naur Form) grammar is a low-level decomposition of the stream.  This grammar describes a syntactically well-formed stream, but not one which is necessarily a valid print job.  (A revised grammar incorporating rules for a valid print job is given in the next section.)  The symbols <DWORD(n)> denote a sequence of zero or more DWORDs whose contents are dependent on the chunk type.


<zj_stream> ::= <zj_signature> <zj_chunk>+

<zj_signature> ::= <DWORD:'ZJZJ'> | <DWORD:'JZJZ'>
{ integers are big-Endian iff == 'ZJZJ' }

<zj_chunk> ::= <zj_header(Type)> <zj_chunkTail(Type)>? 

<zj_header(Type)> ::= <DWORD:chunkSize> <Type:chunkType>
                      <DWORD:dwParam> <WORD:0>
                      <WORD:'ZZ'> 

  { Note the fixed size header for parsing ease }  

<zj_chunkTail(Type)> ::= <DWORD(n)>

Basic parsing of the chunk is very simple and a Zj-stream reader can easily skip chunks it does not understand.  The example code in Listing 1 skips through a Zj-stream simply counting the chunks (or the tokens less one).

Listing 1.  A very simple ZJS example in C++ 

// count ZJS chunks - simplest ZJS util 
#include <fstream.h> 
 
struct ZjEndian { 
     int _byteSwapped; 
     ZjEndian(unsigned long dw) {
          if (0x5A4A5A4AL == dw) _byteSwapped = 1; 
          else if (0x4A5A4A5AL == dw) _byteSwapped = 0;
          else throw "no ZJ signature detected"; 
          } 
     long rectify(long dw) const
          if (!_byteSwapped) return dw; 
          char result[4] = { ((char*) &dw)[3], ((char*) &dw)[2], 
               ((char*) &dw)[1], ((char*) &dw)[0] }; 
          return (*((long*) result)); 
          } 
}; 
 
void main(int argc, char* argv[]) { 
     int chunkCount = 0; 
     try
          if (argc < 2) throw "specify a file"; 
          ifstream zjsFile(argv[1], ios::in | ios::binary |
                ios::nocreate); 
          if (!zjsFile.is_open()) throw "can't open input file"; 
          long dw; 
          zjsFile.read((char*) &dw, sizeof(long)); 
          ZjEndian zjEndian(dw); 
          for ( ; !zjsFile.eof(); chunkCount++ ) { 
                zjsFile.read((char*) &dw, sizeof(long));
             zjsFile.seekg(zjEndian.rectify(dw) - sizeof(long),
                   ios::cur); 
              } 
          zjsFile.close(); 
     } 
     catch (const char* s) 
          { cout << "error: " << s << endl; } 
     cout << "counted " << chunkCount << " chunks" << endl
}

Each chunk is organized as 16 bytes followed by an integral number of DWORDs (total chunk size is 4n bytes, where n >= 4).  This effectively forks the chunk into a head and a tail.  Chunks may have an empty tail.  When present, the format of the tail data is determined by the chunk type (the DWORD chunkType member of the chunk head).  Refer to the "Chunks" section to learn more about the various chunk types, including vendor-defined chunks.

Chunk Head

16 bytes

DWORD totalChunkSize
DWORD chunkType
DWORD dwParam (# Items)
WORD reserved (0)
WORD chunk signature ("ZZ")

Chunk Tail

totalChunkSize - 16 bytes

 

data

Chunk Organization

Since the size of the tail may be deduced from the head, the data of the tail may be transferred by a very simple handler with no further interpretation, making it well-suited for a DMA (direct memory access) transfer.

In the Zj-RCA controller, for example, the onboard CPU never even sees the image data bytes.  This is important because a "handshook" Centronics transfer usually peaks out in the 150Kb/s range, but the Zj-RCA needs data rates which are typically greater than 500Kb/s.  A sufficient data rate can be achieved using standard (un-enhanced) parallel ports, but it requires a special high-speed protocol.  Sending the majority of the data in a "blind" fashion greatly facilitates this because the protocol is handled primarily in the hardware of the controller (as opposed to negotiating every byte in software).

Stream Semantics

In the previous section, we described how to decompose the Zj-stream into chunks.  We were unconcerned with the meaning or ordering of the chunks.  In this section, we describe how chunks/tokens are be combined to form valid print job descriptions.

A valid Zj-stream represents a single collection of page descriptions, which is a print job.  Each page description contains a number of plane descriptions, each of which is a device-resolution raster.  The rasters are combined in a device-specific way to form the final image.

4-byte signature

startDoc
    startPage (1)
        planeDescriptions
    endPage (1)
    startPage (2)
        planeDescriptions
    endPage (2)
    ...
    startPage (n)
        planeDescriptions
    endPage (n)
endDoc

Zj-Stream Structural Organization

Plane descriptions are somewhat loosely defined because the intent is to allow the driver and controller to settle on a method which is convenient primarily for the controller.  Many variations are possible, such as multiple bits per pixel, interlacing, full-plane versus banded imaging, and so on.  The Zj-stream format itself places few restrictions on the plane description method.

The following EBNF is a high-level decomposition of an entire Zj-stream.  

<zj_stream> ::= <signatureToken> <startDoc> 
                <page>+ <endDoc>
 

<page> ::= <startPage> <image> <endPage> 

<image> ::= <fullImage> | <bandedJbig> | <bandedRaw> 

<fullImage> ::= <startPlane> <imagePlane> <endPlane> 

<imagePlane> ::= <jbigImage> | <rawImage> 

<jbigImage> ::= <jbigHeader> <jbigBlock>+ <jbigTail>

<rawImage> ::= <rawHeader> <rawBlock>+ <rawTail>

<bandedJbig> ::= <bandedJbigHead> <bandedJbigPlane>+ 

<bandedJbigHead> ::= <startPlane> <jbigImageHeader>
                     <endPlane> 

<bandedJbigPlane> ::= <startPlane> <jbigBlock>
                      <endPlane> 

<bandedRaw> ::= <bandedRawHead> <bandedRawPlane>+

<bandedRawHead> ::= <startPlane> <rawImageHeader>
                    <endPlane> 

<bandedRawPlane> ::= <startPlane> <rawBlock> <endPlane>

Chunks with DWORD chunkType values between 1024 and 4,294,967,295 are considered "comment" tokens and should be ignored.  Vendors can reserve blocks within that range to use for passing private data between the host driver and the controller.  See the "Chunks" section for more details.  In the above grammar, comment chunks are ignored (removed during lexical analysis).

Note that the "banded" image encodings described above operate at the ZJ chunk level, which may or may not correspond to image bands.  In other words, the bands do not necessarily break at regular intervals in the image or even on pixel or scan line boundaries.  ZJ JBIG image blocks, for example, can contain one or more JBIG BID elements, and the data for any given JBIG BID element may be stretched across multiple ZJ JBIG image blocks.

Chunks

Chunks come in two basic flavors: with tagged item lists or with binary data lumps -- both of which may be empty (i.e., have a length of zero bytes).  The flavor is determined by the chunk type.

The standard Zeno chunks are described below.  Chunk type codes 0..1023, stored in the DWORD chunkType field, are reserved by Zenographics for defining standard chunk types.  All other chunk type codes should be considered comment chunks.  Chunk type codes within the comment range are allocated to third parties by Zenographics.  These codes will thus be ignored by all systems except those created by the assignee.  As the sole arbiter, Zenographics can assume responsibility for ensuring that private codes do not overlap.

Several non-terminals are not defined in the Zj-stream syntax.  This is because these elements are all of the same form with one exception: a "type" element of each must match the 32-bit value defined for that syntactic element.  The follow psuedo-productions are parameterized accordingly to shorten the notation.

<zj_chunk(Type)> ::= <zj_header(Type)> <chunkItem(Type)>*
                   | <zj_header(Type)> <chunkData(Type)>*  

     {where Type can be ZJT_START_DOC, ZJT_END_DOC, ZJT_START_PAGE,
       ZJT_END_PAGE, ZJT_JBIG_BIH, ZJT_JBIG_HID, ZJT_END_JBIG, ZJT_RAW_IMAGE,
       ZJT_START_PLANE or ZJT_END_PLANE }

<chunkItem(Type)> := <DWORD:itemSize> <WORD:chunkType>
                     <BYTE:itemType> <BYTE:bParam> <DWORD(n)>

     {where itemType can be ZJIT_UINT32, ZJIT_INT32, ZJIT_STRING or
       ZJ_BYTELUT

<chunkItem(Type)> := <DWORD(n)>

These will be expanded and explained in detail later, but this representation is useful because it suggests the extremely simple algorithm for parsing the stream.  The Zj-stream consists at the highest level of a 4-byte signature, followed byte a series of tagged "chunks."  Since the chunks have a fixed header that includes the size of the entire chunk, a parser can ignore chunks which it does not understand.   

A simple parsing algorithm is shown below.  Rather than relying on the stream "EOF," a boolean flag could be used to detect the ZJT_END_DOC chunk and exit the loop then.

zjs_signature = ReadStream(s, sizeof(ZjsSignature)); /*4 bytes*/ 
while (!EndOfStream(s)) { 
     chunkHeader = ReadStream(s, sizeof(ZjsHeader)); /*16 bytes*/ 
     restOfChunk = ReadStream(s, chunkHeader.size -
           sizeof(ZjsHeader)); 
     switch (chunkHeader.Type) { 
          /*case for each understood type*/ 
          ... default: /*ignore the chunk*/ 
          break
     } 
}

Note that this sequencing integrates very well with a simple high-speed data communications protocol.

Another important feature is that all transfers are aligned to 32-bits.  Data items are not, however, always aligned to 32-bits.  Byte swapping must take this into account when dealing with individual items.  Specifically, we cannot simply DWORD byte-swap everything.  The first 3 DWORDs of the chunk header must be DWORD byte-swapped (if necessary), and the following 2 WORD's must be individually WORD byte-swapped (if necessary).

Each data entity of the Zj-Stream take the form of a chunk with a fixed head and variable tail.  The head (explained above) provides generic information about the chunk, including its type.  Its interpretation is relatively static.

The interpretation of the tail varies more.  Generically, the tail is broken into two parts: a list of "items" followed by some binary data.   The list of items expected or allowed depends on the chunk type, as does the binary data.  Not all chunk types will have any items or binary data, both are optional.   Some chunks consist solely of the 16-byte head.

 

Chunk Head

16 bytes

(explained above) totalChunkSize
chunkType
dwParam (# Items)
reserved chunk signature
 

Chunk Tail

may contain items or data, but NEVER BOTH

Item List

may be empty

item 1

...
item n

Data Block

may be empty

data ...

Chunk Header Organization