|
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
|