//C- -*- C++ -*-
//C- -------------------------------------------------------------------
//C- DjVuLibre-3.5
//C- Copyright (c) 2002 Leon Bottou and Yann Le Cun.
//C- Copyright (c) 2001 AT&T
//C-
//C- This software is subject to, and may be distributed under, the
//C- GNU General Public License, either Version 2 of the license,
//C- or (at your option) any later version. The license should have
//C- accompanied the software or you may obtain a copy of the license
//C- from the Free Software Foundation at http://www.fsf.org .
//C-
//C- This program is distributed in the hope that it will be useful,
//C- but WITHOUT ANY WARRANTY; without even the implied warranty of
//C- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//C- GNU General Public License for more details.
//C-
//C- DjVuLibre-3.5 is derived from the DjVu(r) Reference Library from
//C- Lizardtech Software. Lizardtech Software has authorized us to
//C- replace the original DjVu(r) Reference Library notice by the following
//C- text (see doc/lizard2002.djvu and doc/lizardtech2007.djvu):
//C-
//C- ------------------------------------------------------------------
//C- | DjVu (r) Reference Library (v. 3.5)
//C- | Copyright (c) 1999-2001 LizardTech, Inc. All Rights Reserved.
//C- | The DjVu Reference Library is protected by U.S. Pat. No.
//C- | 6,058,214 and patents pending.
//C- |
//C- | This software is subject to, and may be distributed under, the
//C- | GNU General Public License, either Version 2 of the license,
//C- | or (at your option) any later version. The license should have
//C- | accompanied the software or you may obtain a copy of the license
//C- | from the Free Software Foundation at http://www.fsf.org .
//C- |
//C- | The computer code originally released by LizardTech under this
//C- | license and unmodified by other parties is deemed "the LIZARDTECH
//C- | ORIGINAL CODE." Subject to any third party intellectual property
//C- | claims, LizardTech grants recipient a worldwide, royalty-free,
//C- | non-exclusive license to make, use, sell, or otherwise dispose of
//C- | the LIZARDTECH ORIGINAL CODE or of programs derived from the
//C- | LIZARDTECH ORIGINAL CODE in compliance with the terms of the GNU
//C- | General Public License. This grant only confers the right to
//C- | infringe patent claims underlying the LIZARDTECH ORIGINAL CODE to
//C- | the extent such infringement is reasonably necessary to enable
//C- | recipient to make, have made, practice, sell, or otherwise dispose
//C- | of the LIZARDTECH ORIGINAL CODE (or portions thereof) and not to
//C- | any greater extent that may be necessary to utilize further
//C- | modifications or combinations.
//C- |
//C- | The LIZARDTECH ORIGINAL CODE is provided "AS IS" WITHOUT WARRANTY
//C- | OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
//C- | TO ANY WARRANTY OF NON-INFRINGEMENT, OR ANY IMPLIED WARRANTY OF
//C- | MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.
//C- +------------------------------------------------------------------
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif
#if NEED_GNUG_PRAGMAS
# pragma implementation
#endif
/** @name djvumake
{\bf Synopsis}
\begin{verbatim}
% djvumake <djvufile> [...<arguments>...]
\end{verbatim}
{\bf Description}
This command assembles a single-page DjVu file by copying or creating
chunks according to the provided <arguments>. Supported syntaxes for
<arguments> are as follows:
\begin{tabular}{ll}
{#INFO=<w>,<h>,<dpi>#} &
Creates the initial ``INFO'' chunk. Arguments #w#, #h# and #dpi#
describe the width, height and resolution of the image. All arguments
may be omitted. The default resolution is 300 dpi. The default width
and height will be retrieved from the first mask chunk specified in the
command line options.\\
{#Sjbz=<jb2file>#} &
Creates a JB2 mask chunk. File #jb2file# may
contain raw JB2 data or be a DjVu file containing JB2 data, such as the
files produced by \Ref{cjb2}.\\
{#Smmr=<mmrfile>] #} &
Creates a mask chunk containing MMR/G4 data. File #mmrfile#
may contain raw MMR data or be a DjVu file containing MMR data.\\
{#BG44=<iw44file>:<n>#} &
Creates one or more IW44 background chunks. File #iw44file# must be an
IW44 file such as the files created by \Ref{c44}. The optional argument
#n# indicates the number of chunks to copy from the IW44 file.\\
{#BGjp=<jpegfile>#} &
Creates a JPEG background chunk.\\
{#BG2k=<jpegfile>#} &
Creates a JPEG-2000 background chunk.\\
{#FG44=<iw44file>#} &
Creates one IW44 foreround chunks. File #iw44file# must be an
IW44 file such as the files created by \Ref{c44}. Only the first
chunk will be copied.\\
{FGbz=(bzzfile|\{#color1[:x,y,w,h]\})}
Creates a chunk containing colors for each JB2 encoded object.
Such chunks are created using class \Ref{DjVuPalette}.
See program \Ref{cpaldjvu} for an example.\\
{#FGjp=<jpegfile>#} &
Creates a JPEG foreground chunk.\\
{#FG2k=<jpegfile>#} &
Creates a JPEG-2000 foreground chunk.\\
{#INCL=<fileid>#} &
Creates an include chunk pointing to <fileid>.
The resulting file should then be included into a
multipage document.\\
{#PPM=<ppmfile>#} (psuedo-chunk) &
Create IW44 foreground and background chunks
by masking and subsampling PPM file #ppmfile#.
This is used by program \Ref{cdjvu}.\\
{#chunk=<rawdatafile>#} &
Creates the specified chunk with the specified raw data.
\end{tabular}
Let us assume now that you have a PPM image #"myimage.ppm"# and a PBM
bitonal image #"mymask.pbm"# whose black pixels indicate which pixels
belong to the foreground. Such a bitonal file may be obtained by
thresholding, although more sophisticated techniques can give better
results. You can then generate a Compound DjVu File by typing:
\begin{verbatim}
% cjb2 mymask.pbm mymask.djvu
% djvumake mycompound.djvu Sjbz=mymask.djvu PPM=myimage.ppm
\end{verbatim}
@memo
Assemble DjVu files.
@author
L\'eon Bottou <leonb@research.att.com> \\
Patrick Haffner <haffner@research.att.com>
*/
//@{
//@}
#include "GString.h"
#include "GException.h"
#include "DjVuImage.h"
#include "MMRDecoder.h"
#include "IFFByteStream.h"
#include "JB2Image.h"
#include "IW44Image.h"
#include "GPixmap.h"
#include "GBitmap.h"
#include "GContainer.h"
#include "GRect.h"
#include "DjVuMessage.h"
#include "common.h"
int flag_contains_fg = 0;
int flag_contains_bg = 0;
int flag_contains_stencil = 0;
int flag_contains_incl = 0;
int flag_fg_needs_palette = 0;
struct DJVUMAKEGlobal
{
// Globals that need static initialization
// are grouped here to work around broken compilers.
GP<ByteStream> jb2stencil;
GP<ByteStream> mmrstencil;
GP<JB2Image> stencil;
GP<JB2Dict> dictionary;
GTArray<GRect> colorzones;
GP<ByteStream> colorpalette;
};
static DJVUMAKEGlobal& g(void)
{
static DJVUMAKEGlobal g;
return g;
}
int w = -1;
int h = -1;
int dpi = 300;
int blit_count = -1;
// -- Display brief usage information
void
usage()
{
DjVuPrintErrorUTF8(
#ifdef DJVULIBRE_VERSION
"DJVUMAKE --- DjVuLibre-" DJVULIBRE_VERSION "\n"
#endif
"Utility for manually assembling DjVu files\n\n"
"Usage: djvumake djvufile ...arguments...\n"
"\n"
"The arguments describe the successive chunks of the DJVU file.\n"
"Possible arguments are:\n"
"\n"
" INFO=w[,[h[,[dpi]]]] -- Create the initial information chunk\n"
" Sjbz=jb2file -- Create a JB2 mask chunk\n"
" Djbz=jb2file -- Create a JB2 shape dictionary\n"
" Smmr=mmrfile -- Create a MMR mask chunk\n"
" BG44=[iw4file][:nchunks] -- Create one or more IW44 background chunks\n"
" BGjp=jpegfile -- Create a JPEG background chunk\n"
" BG2k=jpeg2000file -- Create a JP2K background chunk\n"
" FG44=iw4file -- Create an IW44 foreground chunk\n"
" FGbz=bzzfile -- Create a foreground color chunk from a file\n"
" FGbz={#color:x,y,w,h} -- Create a foreground color chunk from zones\n"
" FGjp=jpegfile -- Create a JPEG foreground image chunk\n"
" FG2k=jpeg2000file -- Create a JP2K foreground image chunk\n"
" INCL=fileid -- Create an INCL chunk\n"
" chunk=rawdatafile -- Create the specified chunk from the raw data file\n"
" PPM=ppmfile -- Create IW44 foreground and background chunks\n"
" by masking and subsampling a PPM file.\n"
"\n"
"You may omit the specification of the information chunk. An information\n"
"chunk will be created using the image size of the first mask chunk\n"
"This program is sometimes able to issue a warning when you are building an\n"
"incorrect djvu file.\n"
"\n");
exit(1);
}
// -- Obtain image size from mmr chunk
void
analyze_mmr_chunk(const GURL &url)
{
if (!g().mmrstencil || !g().mmrstencil->size())
{
GP<ByteStream> gbs=ByteStream::create(url,"rb");
ByteStream &bs=*gbs;
g().mmrstencil = ByteStream::create();
// Check if file is an IFF file
char magic[4];
memset(magic,0,sizeof(magic));
bs.readall(magic,sizeof(magic));
if (!GStringRep::cmp(magic,"AT&T",4))
bs.readall(magic,sizeof(magic));
if (GStringRep::cmp(magic,"FORM",4))
{
// Must be a raw file
bs.seek(0);
g().mmrstencil->copy(bs);
}
else
{
// Search Smmr chunk
bs.seek(0);
GUTF8String chkid;
GP<IFFByteStream> giff=IFFByteStream::create(gbs);
IFFByteStream &iff=*giff;
if (iff.get_chunk(chkid)==0 || chkid!="FORM:DJVU")
G_THROW("Expecting a DjVu file!");
for(; iff.get_chunk(chkid); iff.close_chunk())
if (chkid=="Smmr") { g().mmrstencil->copy(bs); break; }
}
// Check result
g().mmrstencil->seek(0);
if (!g().mmrstencil->size())
G_THROW("Could not find MMR data");
// Decode
g().stencil = MMRDecoder::decode(g().mmrstencil);
int jw = g().stencil->get_width();
int jh = g().stencil->get_height();
if (w < 0) w = jw;
if (h < 0) h = jh;
if (jw!=w || jh!=h)
DjVuPrintErrorUTF8("djvumake: mask size (%s) does not match info size\n", (const char *)url);
}
}
// -- Obtain shape dictionary
void
analyze_djbz_chunk(GP<ByteStream> gbs)
{
if (g().dictionary)
G_THROW("Duplicate Djbz dictionary");
g().dictionary = JB2Dict::create();
g().dictionary->decode(gbs);
}
void
analyze_djbz_chunk(const GURL &url)
{
GP<ByteStream> gbs = ByteStream::create(url, "rb");
analyze_djbz_chunk(gbs);
}
// -- Obtain image size and blit count from jb2 chunk
GP<JB2Dict>
provide_shared_dict( void* )
{
return(g().dictionary);
}
void
analyze_jb2_chunk(const GURL &url)
{
if (!g().jb2stencil || !g().jb2stencil->size())
{
GP<ByteStream> gbs=ByteStream::create(url,"rb");
ByteStream &bs=*gbs;
g().jb2stencil = ByteStream::create();
// Check if file is an IFF file
char magic[4];
memset(magic,0,sizeof(magic));
bs.readall(magic,sizeof(magic));
if (!GStringRep::cmp(magic,"AT&T",4))
bs.readall(magic,sizeof(magic));
if (GStringRep::cmp(magic,"FORM",4))
{
// Must be a raw file
bs.seek(0);
g().jb2stencil->copy(bs);
}
else
{
// Search Sjbz chunk
bs.seek(0);
GUTF8String chkid;
GP<IFFByteStream> giff=IFFByteStream::create(gbs);
IFFByteStream &iff=*giff;
if (iff.get_chunk(chkid)==0 || chkid!="FORM:DJVU")
G_THROW("Expecting a DjVu file!");
for(; iff.get_chunk(chkid); iff.close_chunk())
if (chkid=="Sjbz") { g().jb2stencil->copy(bs); break; }
}
// Check result
g().jb2stencil->seek(0);
if (!g().jb2stencil->size())
G_THROW("Could not find JB2 data");
// Decode
g().stencil=JB2Image::create();
g().stencil->decode(g().jb2stencil,&provide_shared_dict,NULL);
int jw = g().stencil->get_width();
int jh = g().stencil->get_height();
if (w < 0)
w = jw;
if (h < 0)
h = jh;
if (blit_count < 0)
blit_count = g().stencil->get_blit_count();
if (jw!=w || jh!=h)
DjVuPrintErrorUTF8("djvumake: mask size (%s) does not match info size\n", (const char *)url);
}
}
// -- Load dictionary from an INCL chunk
void
analyze_incl_chunk(const GURL &url)
{
GP<ByteStream> gbs = ByteStream::create(url,"rb");
char buffer[24];
memset(buffer, 0, sizeof(buffer));
gbs->read(buffer,sizeof(buffer));
char *s = buffer;
if (!strncmp(s, "AT&T", 4))
s += 4;
if (strncmp(s, "FORM", 4) || strncmp(s+8, "DJVI", 4))
G_THROW("Expecting a valid FORM:DJVI chunk in the included file");
gbs->seek(0);
GP<IFFByteStream> giff=IFFByteStream::create(gbs);
GUTF8String chkid;
giff->get_chunk(chkid); // FORM:DJVI
for(; giff->get_chunk(chkid); giff->close_chunk())
if (chkid=="Djbz")
analyze_djbz_chunk(giff->get_bytestream());
}
void
check_for_shared_dict(GArray<GUTF8String> &argv)
{
const int argc=argv.hbound()+1;
for (int i=2; i<argc; i++)
if (!argv[i].cmp("INCL=",5))
analyze_incl_chunk(GURL::Filename::UTF8(5+(const char *)argv[i]));
else if (!argv[i].cmp("Djbz=", 5))
analyze_djbz_chunk(GURL::Filename::UTF8(5+(const char *)argv[i]));
}
// -- Create info chunk from specification or mask
void
create_info_chunk(IFFByteStream &iff, GArray<GUTF8String> &argv)
{
const int argc=argv.hbound()+1;
// Process info specification
for (int i=2; i<argc; i++)
if (!argv[i].cmp("INFO=",5))
{
int narg = 0;
const char *ptr = 5+(const char *)argv[i];
while (*ptr)
{
if (*ptr != ',')
{
int x = strtol((char *)ptr, (char **)&ptr, 10);
switch(narg)
{
case 0:
w = x; break;
case 1:
h = x; break;
case 2:
dpi = x; break;
default:
G_THROW("djvumake: incorrect 'INFO' chunk specification\n");
}
}
narg++;
if (*ptr && *ptr++!=',')
G_THROW("djvumake: comma expected in 'INFO' chunk specification\n");
}
break;
}
if (w>0 && (w<=0 || w>=32768))
G_THROW("djvumake: incorrect width in 'INFO' chunk specification\n");
if (h>0 && (h<=0 || h>=32768))
G_THROW("djvumake: incorrect height in 'INFO' chunk specification\n");
if (dpi>0 && (dpi<25 || dpi>6000))
G_THROW("djvumake: incorrect dpi in 'INFO' chunk specification\n");
// Search first mask chunks if size is still unknown
if (h<0 || w<0)
{
for (int i=2; i<argc; i++)
if (!argv[i].cmp("Sjbz=",5))
{
analyze_jb2_chunk(GURL::Filename::UTF8(5+(const char *)argv[i]));
break;
}
else if (!argv[i].cmp("Smmr=",5))
{
analyze_mmr_chunk(GURL::Filename::UTF8(5+(const char *)argv[i]));
break;
}
}
// Check that we have everything
if (w<0 || h<0)
G_THROW("djvumake: cannot determine image size\n");
// write info chunk
GP<DjVuInfo> ginfo=DjVuInfo::create();
DjVuInfo &info=*ginfo;
info.width = w;
info.height = h;
info.dpi = dpi;
iff.put_chunk("INFO");
info.encode(*iff.get_bytestream());
iff.close_chunk();
}
// -- Create MMR mask chunk
void
create_mmr_chunk(IFFByteStream &iff, const char *chkid, const GURL &url)
{
analyze_mmr_chunk(url);
g().mmrstencil->seek(0);
iff.put_chunk(chkid);
iff.copy(*g().mmrstencil);
iff.close_chunk();
}
// -- Create FGbz palette chunk
void
create_fgbz_chunk(IFFByteStream &iff)
{
int nzones = g().colorzones.size();
int npalette = g().colorpalette->size() / 3;
GP<DjVuPalette> pal = DjVuPalette::create();
g().colorpalette->seek(0);
pal->decode_rgb_entries(*g().colorpalette, npalette);
pal->colordata.resize(0,blit_count-1);
for (int d=0; d<blit_count; d++)
{
JB2Blit *blit = g().stencil->get_blit(d);
const JB2Shape &shape = g().stencil->get_shape(blit->shapeno);
GRect brect(blit->left, blit->bottom, shape.bits->columns(), shape.bits->rows());
int index = nzones;
for (int i=0; i<nzones; i++)
{
GRect zrect = g().colorzones[i];
if (zrect.isempty() || zrect.intersect(brect, zrect))
index = i;
}
if (index >= npalette)
G_THROW("create_fgbz_chunk: internal error");
pal->colordata[d] = index;
}
iff.put_chunk("FGbz");
pal->encode(iff.get_bytestream());
iff.close_chunk();
}
// -- Create JB2 mask chunk
void
create_jb2_chunk(IFFByteStream &iff, const char *chkid, const GURL &url)
{
analyze_jb2_chunk(url);
g().jb2stencil->seek(0);
iff.put_chunk(chkid);
iff.copy(*g().jb2stencil);
iff.close_chunk();
}
// -- Create inclusion chunk
void
create_incl_chunk(IFFByteStream &iff, const char *chkid, const char *fileid)
{
iff.put_chunk("INCL");
iff.write(fileid, strlen(fileid));
iff.close_chunk();
}
// -- Create chunk by copying file contents
void
create_raw_chunk(IFFByteStream &iff, const GUTF8String &chkid, const GURL &url)
{
iff.put_chunk(chkid);
GP<ByteStream> ibs=ByteStream::create(url,"rb");
iff.copy(*ibs);
iff.close_chunk();
}
// -- Internal headers for IW44
struct PrimaryHeader {
unsigned char serial;
unsigned char slices;
} primary;
struct SecondaryHeader {
unsigned char major;
unsigned char minor;
unsigned char xhi, xlo;
unsigned char yhi, ylo;
} secondary;
// -- Create and check FG44 chunk
void
create_fg44_chunk(IFFByteStream &iff, const char *ckid, const GURL &url)
{
GP<ByteStream> gbs=ByteStream::create(url,"rb");
GP<IFFByteStream> gbsi=IFFByteStream::create(gbs);
IFFByteStream &bsi=*gbsi;
GUTF8String chkid;
bsi.get_chunk(chkid);
if (chkid != "FORM:PM44" && chkid != "FORM:BM44")
G_THROW("djvumake: FG44 file has incorrect format (wrong IFF header)");
bsi.get_chunk(chkid);
if (chkid!="PM44" && chkid!="BM44")
G_THROW("djvumake: FG44 file has incorrect format (wring IFF header)");
GP<ByteStream> gmbs=ByteStream::create();
ByteStream &mbs=*gmbs;
mbs.copy(*bsi.get_bytestream());
bsi.close_chunk();
if (bsi.get_chunk(chkid))
DjVuPrintErrorUTF8("%s","djvumake: FG44 file contains more than one chunk\n");
bsi.close_chunk();
mbs.seek(0);
if (mbs.readall((void*)&primary, sizeof(primary)) != sizeof(primary))
G_THROW("djvumake: FG44 file is corrupted (cannot read primary header)");
if (primary.serial != 0)
G_THROW("djvumake: FG44 file is corrupted (wrong serial number)");
if (mbs.readall((void*)&secondary, sizeof(secondary)) != sizeof(secondary))
G_THROW("djvumake: FG44 file is corrupted (cannot read secondary header)");
int iw = (secondary.xhi<<8) + secondary.xlo;
int ih = (secondary.yhi<<8) + secondary.ylo;
int red;
for (red=1; red<=12; red++)
if (iw==(w+red-1)/red && ih==(h+red-1)/red)
break;
flag_contains_fg = red;
if (red>12)
DjVuPrintErrorUTF8("%s","djvumake: FG44 subsampling is not in [1..12] range\n");
mbs.seek(0);
iff.put_chunk(ckid);
iff.copy(mbs);
iff.close_chunk();
}
// -- Create and check BG44 chunk
void
create_bg44_chunk(IFFByteStream &iff, const char *ckid, GUTF8String filespec)
{
static GP<IFFByteStream> bg44iff;
if (! bg44iff)
{
if (flag_contains_bg)
DjVuPrintErrorUTF8("%s","djvumake: Duplicate BGxx chunk\n");
int i=filespec.rsearch(':');
for (int j=i+1; i>0 && j<(int)filespec.length(); j++)
if (filespec[j] < '0' || filespec[j] > '9')
i = -1;
if (!i)
G_THROW("djvumake: no filename specified in first BG44 specification");
GUTF8String filename=(i<0)?filespec:GUTF8String(filespec, i);
const GURL::Filename::UTF8 url(filename);
const GP<ByteStream> gbs(ByteStream::create(url,"rb"));
if(!gbs)
{
G_THROW("djvumake: no such file as"+filename);
}
bg44iff = IFFByteStream::create(gbs);
GUTF8String chkid;
bg44iff->get_chunk(chkid);
if (chkid != "FORM:PM44" && chkid != "FORM:BM44")
G_THROW("djvumake: BG44 file has incorrect format (wrong IFF header)");
if (i>=0)
filespec = i+1+(const char *)filespec;
else
filespec = "99";
}
else
{
if (filespec.length() && filespec[0]!=':')
G_THROW("djvumake: filename specified in BG44 refinement");
filespec = 1+(const char *)filespec;
}
const char *s=filespec;
int nchunks = strtol((char *)s, (char **)&s, 10);
if (nchunks<1 || nchunks>99)
G_THROW("djvumake: invalid number of chunks in BG44 specification");
if (*s)
G_THROW("djvumake: invalid BG44 specification (syntax error)");
int flag = (nchunks>=99);
GUTF8String chkid;
while (nchunks-->0 && bg44iff->get_chunk(chkid))
{
if (chkid!="PM44" && chkid!="BM44")
{
DjVuPrintErrorUTF8("%s","djvumake: BG44 file contains unrecognized chunks (fixed)\n");
nchunks += 1;
bg44iff->close_chunk();
continue;
}
GP<ByteStream> gmbs=ByteStream::create();
ByteStream &mbs=*gmbs;
mbs.copy(*(bg44iff->get_bytestream()));
bg44iff->close_chunk();
mbs.seek(0);
if (mbs.readall((void*)&primary, sizeof(primary)) != sizeof(primary))
G_THROW("djvumake: BG44 file is corrupted (cannot read primary header)\n");
if (primary.serial == 0)
{
if (mbs.readall((void*)&secondary, sizeof(secondary)) != sizeof(secondary))
G_THROW("djvumake: BG44 file is corrupted (cannot read secondary header)\n");
int iw = (secondary.xhi<<8) + secondary.xlo;
int ih = (secondary.yhi<<8) + secondary.ylo;
int red;
for (red=1; red<=12; red++)
if (iw==(w+red-1)/red && ih==(h+red-1)/red)
break;
flag_contains_bg = red;
if (red>12)
DjVuPrintErrorUTF8("%s","djvumake: BG44 subsampling is not in [1..12] range\n");
}
mbs.seek(0);
iff.put_chunk(ckid);
iff.copy(mbs);
iff.close_chunk();
flag = 1;
}
if (!flag)
DjVuPrintErrorUTF8("%s","djvumake: no more chunks in BG44 file\n");
}
// -- Forward declarations
void processForeground(const GPixmap* image, const JB2Image *mask,
GPixmap& subsampled_image, GBitmap& subsampled_mask);
void processBackground(const GPixmap* image, const JB2Image *mask,
GPixmap& subsampled_image, GBitmap& subsampled_mask);
// -- Create both foreground and background by masking and subsampling
void
create_masksub_chunks(IFFByteStream &iff, const GURL &url)
{
// Check and load pixmap file
if (!g().stencil)
G_THROW("The use of a raw ppm image requires a stencil");
GP<ByteStream> gibs=ByteStream::create(url, "rb");
ByteStream &ibs=*gibs;
GP<GPixmap> graw_pm=GPixmap::create(ibs);
GPixmap &raw_pm=*graw_pm;
if ((int) g().stencil->get_width() != (int) raw_pm.columns())
G_THROW("Stencil and raw image have different widths!");
if ((int) g().stencil->get_height() != (int) raw_pm.rows())
G_THROW("Stencil and raw image have different heights!");
// Encode foreground
{
GP<GPixmap> gfg_img=GPixmap::create();
GPixmap &fg_img=*gfg_img;
GP<GBitmap> fg_mask=GBitmap::create();
processForeground(&raw_pm, g().stencil, fg_img, *fg_mask);
GP<IW44Image> fg_pm = IW44Image::create_encode(fg_img, fg_mask, IW44Image::CRCBfull);
IWEncoderParms parms[8];
iff.put_chunk("FG44");
parms[0].slices = 100;
fg_pm->encode_chunk(iff.get_bytestream(), parms[0]);
iff.close_chunk();
}
// Encode backgound
{
GP<GPixmap> gbg_img=GPixmap::create();
GPixmap &bg_img=*gbg_img;
GP<GBitmap> bg_mask=GBitmap::create();
processBackground(&raw_pm, g().stencil, bg_img, *bg_mask);
GP<IW44Image> bg_pm = IW44Image::create_encode(bg_img, bg_mask, IW44Image::CRCBnormal);
IWEncoderParms parms[4];
parms[0].bytes = 10000;
parms[0].slices = 74;
iff.put_chunk("BG44");
bg_pm->encode_chunk(iff.get_bytestream(), parms[0]);
iff.close_chunk();
parms[1].slices = 84;
iff.put_chunk("BG44");
bg_pm->encode_chunk(iff.get_bytestream(), parms[1]);
iff.close_chunk();
parms[2].slices = 90;
iff.put_chunk("BG44");
bg_pm->encode_chunk(iff.get_bytestream(), parms[2]);
iff.close_chunk();
parms[3].slices = 97;
iff.put_chunk("BG44");
bg_pm->encode_chunk(iff.get_bytestream(), parms[3]);
iff.close_chunk();
}
}
const char *
parse_color_name(const char *s, char *rgb)
{
static struct {
const char *name;
unsigned char r, g, b;
} stdcols[] = {
{"aqua", 0x00, 0xFF, 0xFF},
{"black", 0x00, 0x00, 0x00},
{"blue", 0x00, 0x00, 0xFF},
{"fuchsia", 0xFF, 0x00, 0xFF},
{"gray", 0x80, 0x80, 0x80},
{"green", 0x00, 0x80, 0x00},
{"lime", 0x00, 0xFF, 0x00},
{"maroon", 0x80, 0x00, 0x00},
{"navy", 0x00, 0x00, 0x80},
{"olive", 0x80, 0x80, 0x00},
{"purple", 0x80, 0x00, 0x80},
{"red", 0xFF, 0x00, 0x00},
{"silver", 0xC0, 0xC0, 0xC0},
{"teal", 0x00, 0x80, 0x80},
{"white", 0xFF, 0xFF, 0xFF},
{"yellow", 0xFF, 0xFF, 0x00},
{0}
};
// potential color names
int len = 0;
while (s[len] && s[len]!=':' && s[len]!='#')
len += 1;
GUTF8String name(s, len);
name = name.downcase();
for (int i=0; stdcols[i].name; i++)
if (name == stdcols[i].name)
{
rgb[0] = stdcols[i].r;
rgb[1] = stdcols[i].g;
rgb[2] = stdcols[i].b;
return s+len;
}
// potential hex specifications
unsigned int r,g,b;
if (sscanf(s,"%2x%2x%2x",&r,&g,&b) == 3)
{
rgb[0] = r;
rgb[1] = g;
rgb[2] = b;
return s+6;
}
G_THROW("Unrecognized color name in FGbz chunk specification");
return 0; // win
}
void
parse_color_zones(const char *s)
{
bool fullpage = false;
int zones = 0;
g().colorzones.empty();
g().colorpalette = ByteStream::create();
// zones
while (s[0] == '#')
{
char rgb[3];
GRect rect;
s = parse_color_name(s+1, rgb);
if (s[0] == ':')
{
int c[4];
for (int i=0; i<4; i++)
{
char *e = 0;
c[i] = strtol(s+1, &e, 10);
if (e <= s || (i>=2 && c[i]<0) || (i<3 && e[0]!=','))
G_THROW("Invalid coordinates in FGbz chunk specification");
s = e;
}
rect = GRect(c[0],c[1],c[2],c[3]);
}
if (rect.isempty())
fullpage = true;
g().colorpalette->writall(rgb, 3);
g().colorzones.touch(zones);
g().colorzones[zones] = rect;
zones++;
}
if (s[0])
G_THROW("Syntax error in FGbz chunk specification");
// add extra black palette entry
if (! fullpage)
{
char rgb[3] = {0,0,0};
g().colorpalette->writall(rgb, 3);
}
}
// -- Main
int
main(int argc, char **argv)
{
DJVU_LOCALE;
GArray<GUTF8String> dargv(0,argc-1);
for(int i=0;i<argc;++i)
dargv[i]=GNativeString(argv[i]);
G_TRY
{
// Print usage when called without enough arguments
if (argc <= 2)
usage();
// Open djvu file
remove(dargv[1]);
GP<IFFByteStream> giff =
IFFByteStream::create(ByteStream::create(GURL::Filename::UTF8(dargv[1]),"wb"));
IFFByteStream &iff=*giff;
// Create header
iff.put_chunk("FORM:DJVU", 1);
// Check if shared dicts are present
check_for_shared_dict(dargv);
// Create information chunk
create_info_chunk(iff, dargv);
// Parse all arguments
for (int i=2; i<argc; i++)
{
if (!dargv[i].cmp("INFO=",5))
{
if (i>2)
DjVuPrintErrorUTF8("%s","djvumake: 'INFO' chunk should appear first (ignored)\n");
}
else if (!dargv[i].cmp("Sjbz=",5))
{
if (flag_contains_stencil)
DjVuPrintErrorUTF8("%s","djvumake: duplicate stencil chunk\n");
create_jb2_chunk(iff, "Sjbz", GURL::Filename::UTF8(5+(const char *)dargv[i]));
flag_contains_stencil = 1;
if (flag_fg_needs_palette && blit_count >= 0)
create_fgbz_chunk(iff);
flag_fg_needs_palette = 0;
}
else if (!dargv[i].cmp("Smmr=",5))
{
create_mmr_chunk(iff, "Smmr",
GURL::Filename::UTF8(5+(const char *)dargv[i]));
if (flag_contains_stencil)
DjVuPrintErrorUTF8("%s","djvumake: duplicate stencil chunk\n");
flag_contains_stencil = 1;
}
else if (!dargv[i].cmp("FGbz=",5))
{
const char *c = 5 + (const char*)dargv[i];
if (flag_contains_fg)
DjVuPrintErrorUTF8("%s","djvumake: duplicate 'FGxx' chunk\n");
if (c[0] != '#')
{
create_raw_chunk(iff, "FGbz", GURL::Filename::UTF8(c));
}
else
{
parse_color_zones(c);
if (flag_contains_stencil && blit_count >= 0)
create_fgbz_chunk(iff);
else
flag_fg_needs_palette = 1;
}
flag_contains_fg = 1;
}
else if (!dargv[i].cmp("FG44=",5))
{
if (flag_contains_fg)
DjVuPrintErrorUTF8("%s","djvumake: duplicate 'FGxx' chunk\n");
create_fg44_chunk(iff, "FG44",
GURL::Filename::UTF8(5+(const char *)dargv[i]));
}
else if (!dargv[i].cmp("BG44=",5))
{
create_bg44_chunk(iff, "BG44", 5+(const char *)dargv[i]);
}
else if (!dargv[i].cmp("BGjp=",5) ||
!dargv[i].cmp("BG2k=",5) )
{
if (flag_contains_bg)
DjVuPrintErrorUTF8("%s","djvumake: Duplicate BGxx chunk\n");
GUTF8String chkid = dargv[i].substr(0,4);
create_raw_chunk(iff, chkid, GURL::Filename::UTF8(5+(const char *)dargv[i]));
flag_contains_bg = 1;
}
else if (!dargv[i].cmp("FGjp=",5) || !dargv[i].cmp("FG2k=",5))
{
if (flag_contains_fg)
DjVuPrintErrorUTF8("%s","djvumake: duplicate 'FGxx' chunk\n");
GUTF8String chkid = dargv[i].substr(0,4);
create_raw_chunk(iff, chkid, GURL::Filename::UTF8(5+(const char *)dargv[i]));
flag_contains_fg = 1;
}
else if (!dargv[i].cmp("INCL=",5))
{
create_incl_chunk(iff, "INCL", GURL::Filename::UTF8(5+(const char *)dargv[i]).fname());
flag_contains_incl = 1;
}
else if (!dargv[i].cmp("PPM=",4))
{
if (flag_contains_bg || flag_contains_fg)
DjVuPrintErrorUTF8("%s","djvumake: Duplicate 'FGxx' or 'BGxx' chunk\n");
create_masksub_chunks(iff, GURL::Filename::UTF8(4+(const char *)dargv[i]));
flag_contains_bg = 1;
flag_contains_fg = 1;
}
else if (dargv[i].length() > 4 && dargv[i][4] == '=')
{
GNativeString chkid = dargv[i].substr(0,4);
if (chkid != "TXTz" && chkid != "TXTa"
&& chkid != "ANTz" && chkid != "ANTa"
&& chkid != "Djbz" )
DjVuPrintErrorUTF8("djvumake: creating chunk of unknown type ``%s''.\n",
(const char*)chkid);
create_raw_chunk(iff, chkid, GURL::Filename::UTF8(5+(const char *)dargv[i]));
}
else
{
DjVuPrintErrorUTF8("djvumake: illegal argument : ``%s'' (ignored)\n",
(const char *)dargv[i]);
}
}
// Common cases for missing chunks
if (flag_contains_stencil)
{
if (flag_contains_bg && ! flag_contains_fg)
{
DjVuPrintErrorUTF8("%s","djvumake: generating black FGbz chunk\n");
g().colorzones.empty();
g().colorpalette = ByteStream::create();
char rgb[3] = {0,0,0};
g().colorpalette->writall(rgb, 3);
create_fgbz_chunk(iff);
flag_contains_fg = 1;
}
if (flag_contains_fg && !flag_contains_bg)
{
DjVuPrintErrorUTF8("%s","djvumake: generating white BG44 chunk\n");
GPixel bgcolor = GPixel::WHITE;
GP<GPixmap> inputsub=GPixmap::create((h+11)/12, (w+11)/12, &bgcolor);
GP<IW44Image> iw = IW44Image::create_encode(*inputsub, 0, IW44Image::CRCBnone);
IWEncoderParms iwparms;
iff.put_chunk("BG44");
iwparms.slices = 97;
iw->encode_chunk(iff.get_bytestream(), iwparms);
iff.close_chunk();
flag_contains_bg = 1;
}
}
// Close
iff.close_chunk();
// Sanity checks
if (flag_contains_stencil)
{
// Compound or Bilevel
if (flag_contains_bg && ! flag_contains_fg)
DjVuPrintErrorUTF8("%s","djvumake: djvu file contains a BGxx chunk but no FGxx chunk\n");
if (flag_contains_fg && ! flag_contains_bg)
DjVuPrintErrorUTF8("%s","djvumake: djvu file contains a FGxx chunk but no BGxx chunk\n");
}
else if (flag_contains_bg)
{
// Photo DjVu Image
if (flag_contains_bg!=1)
DjVuPrintErrorUTF8("%s","djvumake: photo djvu image has subsampled BGxx chunk\n");
if (flag_fg_needs_palette)
DjVuPrintErrorUTF8("%s","djvumake: could not generate FGbz chunk, as stencil is not available\n");
else if (flag_contains_fg)
DjVuPrintErrorUTF8("%s","djvumake: photo djvu file contains FGxx chunk\n");
}
else
DjVuPrintErrorUTF8("%s","djvumake: djvu file contains neither Sxxx nor BGxx chunks\n");
}
G_CATCH(ex)
{
remove(dargv[1]);
ex.perror();
exit(1);
}
G_ENDCATCH;
return 0;
}
////////////////////////////////////////
// MASKING AND SUBSAMPLING
////////////////////////////////////////
// -- Returns a dilated version of a bitmap with the same size
static GP<GBitmap>
dilate8(const GBitmap *p_bm)
{
const GBitmap& bm = *p_bm;
GP<GBitmap> p_newbm = GBitmap::create(bm.rows(),bm.columns(),1);
GBitmap& newbm = *p_newbm;
int nrows = bm.rows();
int ncols = bm.columns();
for(int y=0; y<nrows; y++)
{
const unsigned char *bmrow = bm[y];
unsigned char *nbmprow = (y-1>=0) ? newbm[y-1] : 0;
unsigned char *nbmrow = newbm[y];
unsigned char *nbmnrow = (y+1<nrows) ? newbm[y+1] : 0;
for(int x=0; x<ncols; x++)
{
if(bmrow[x])
{
// Set all the 8-neighborhood to black
if (nbmprow)
{
nbmprow[x-1]=1;
nbmprow[x]=1;
nbmprow[x+1]=1;
}
nbmrow[x-1]=1;
nbmrow[x]=1;
nbmrow[x+1]=1;
if (nbmnrow)
{
nbmnrow[x-1]=1;
nbmnrow[x]=1;
nbmnrow[x+1]=1;
}
}
}
}
return p_newbm;
}
// -- Returns a smaller eroded version of a bitmap
static GP<GBitmap>
erode8(const GBitmap *p_bm)
{
const GBitmap& bm = *p_bm;
int newnrows = bm.rows()-2;
int newncolumns = bm.columns()-2;
if(newnrows<=0 || newncolumns<=0) // then return an empty GBitmap
return GBitmap::create();
GP<GBitmap> p_newbm = GBitmap::create(newnrows,newncolumns);
GBitmap& newbm = *p_newbm;
for(int y=0; y<newnrows; y++)
{
for(int x=0; x<newncolumns; x++)
{
// Check if there's a white pixel in the 8-neighborhood
if( !( bm[y ][x] && bm[y ][x+1] && bm[y ][x+2]
&& bm[y+1][x] && bm[y+1][x+1] && bm[y+1][x+2]
&& bm[y+2][x] && bm[y+2][x+1] && bm[y+2][x+2]))
newbm[y][x] = 0; // then set current to white
else
newbm[y][x] = 1; // else set current to black
}
}
return p_newbm;
}
// -- Returns a smaller eroded version of a jb2image
GP<JB2Image>
erode8(const JB2Image *im)
{
int i;
GP<JB2Image> newim = JB2Image::create();
newim->set_dimension(im->get_width(),im->get_height());
for(i=0; i<im->get_shape_count(); i++)
{
const JB2Shape &shape = im->get_shape(i);
JB2Shape newshape;
newshape.parent = shape.parent;
if (shape.bits)
newshape.bits = erode8(shape.bits);
else
newshape.bits = 0;
newim->add_shape(newshape);
}
for(i=0; i<im->get_blit_count(); i++)
{
const JB2Blit* blit = im->get_blit(i);
JB2Blit newblit;
newblit.bottom = blit->bottom + 1;
newblit.left = blit->left + 1;
newblit.shapeno = blit->shapeno;
newim->add_blit(newblit);
}
return newim;
}
// Subsamples only the pixels of <image> that are not masked (<mask>). This
// call resizes and fills the resulting <subsampled_image> and
// <subsampled_mask>. Their dimension is the dimension of the original
// <image> divided by <gridwidth> and rounded to the superior integer. For
// each square grid (gridwidth times gridwidth) of the subsampling mesh that
// contains at least <minpixels> non-masked pixels, their value is averaged to
// give the value of the corresponding <subsampled_image> pixel, and the
// <subsampled_mask> is cleared at this position. If <inverted_mask> is true,
// then pixels are considered to be masked when mask==0
static void
maskedSubsample(const GPixmap* img,
const GBitmap *p_mask,
GPixmap& subsampled_image,
GBitmap& subsampled_mask,
int gridwidth, int inverted_mask,
int minpixels=1
)
{
const GPixmap& image= *img;
const GBitmap& mask = *p_mask;
int imageheight = image.rows();
int imagewidth = image.columns();
// compute the size of the resulting subsampled image
int subheight = imageheight/gridwidth;
if(imageheight%gridwidth)
subheight++;
int subwidth = imagewidth/gridwidth;
if(imagewidth%gridwidth)
subwidth++;
// set the sizes unless in incremental mode
subsampled_image.init(subheight, subwidth);
subsampled_mask.init(subheight, subwidth);
// go subsampling
int row, col; // row and col in the subsampled image
int posx, posxend, posy, posyend; // corresponding square in the original image
for(row=0, posy=0; row<subheight; row++, posy+=gridwidth)
{
GPixel* subsampled_image_row = subsampled_image[row]; // row row of subsampled image
unsigned char* subsampled_mask_row = subsampled_mask[row]; // row row of subsampled mask
posyend = posy+gridwidth;
if(posyend>imageheight)
posyend = imageheight;
for(col=0, posx=0; col<subwidth; col++, posx+=gridwidth)
{
posxend = posx+gridwidth;
if(posxend>imagewidth)
posxend = imagewidth;
int count = 0;
int r = 0;
int g = 0;
int b = 0;
for(int y=posy; y<posyend; y++)
{
const unsigned char* mask_y = mask[y]; // Row y of the mask
for(int x=posx; x<posxend; x++)
{
unsigned char masked = (inverted_mask ? !mask_y[x] :mask_y[x]);
if(!masked)
{
GPixel p = image[y][x];
r += p.r;
g += p.g;
b += p.b;
count ++;
}
}
}
/* minpixels pixels are enough to give the color */
/* so set it, and do not mask this point */
if(count >= minpixels)
{
GPixel p;
p.r = r/count;
p.g = g/count;
p.b = b/count;
subsampled_image_row[col] = p;
subsampled_mask_row[col] = 0;
}
else /* make it bright red and masked */
{
subsampled_image_row[col] = GPixel::RED;
subsampled_mask_row[col] = 1;
}
}
}
}
// -- Computes foreground image and mask
void
processForeground(const GPixmap* image, const JB2Image *mask,
GPixmap& subsampled_image, GBitmap& subsampled_mask)
{
GP<JB2Image> eroded_mask = erode8(mask);
maskedSubsample(image, eroded_mask->get_bitmap(),
subsampled_image, subsampled_mask,
6, 1); // foreground subsample is 6 (300dpi->50dpi)
}
// -- Computes background image and mask
void
processBackground(const GPixmap* image, const JB2Image *mask,
GPixmap& subsampled_image, GBitmap& subsampled_mask)
{
GP<GBitmap> b = mask->get_bitmap();
b = dilate8(b);
b = dilate8(b);
maskedSubsample(image, b, subsampled_image, subsampled_mask, 3, 0);
}