/*============================================================================= pamtopdbimg =============================================================================== Convert Palm Pilot PDB Image format (for viewing by Pilot Image Viewer) to Netpbm image. Bryan Henderson derived this from Eric Howe's program named 'imgvtopnm', in September 2010. =============================================================================*/ /* * Copyright (C) 1997 Eric A. Howe * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. * * Authors: Eric A. Howe (mu@trends.net) * Bryan Henderson */ #include #include #include "pm_c_util.h" #include "mallocvar.h" #include "nstring.h" #include "shhopt.h" #include "pam.h" #include "ipdb.h" struct cmdlineInfo { /* All the information the user supplied in the command line, in a form easy for the program to use. */ const char * inputFileName; /* '-' if stdin */ const char * notefile; /* NULL if not specified */ unsigned int verbose; }; static void parseCommandLine(int argc, const char ** argv, struct cmdlineInfo * const cmdlineP) { /*---------------------------------------------------------------------------- parse program command line described in Unix standard form by argc and argv. Return the information in the options as *cmdlineP. If command line is internally inconsistent (invalid options, etc.), issue error message to stderr and abort program. Note that the strings we return are stored in the storage that was passed to us as the argv array. We also trash *argv. -----------------------------------------------------------------------------*/ optEntry *option_def; /* Instructions to pm_optParseOptions3 on how to parse our options. */ optStruct3 opt; unsigned int option_def_index; unsigned int notefileSpec; MALLOCARRAY_NOFAIL(option_def, 100); option_def_index = 0; /* incremented by OPTENT3 */ OPTENT3(0, "notefile", OPT_STRING, &cmdlineP->notefile, ¬efileSpec, 0); OPTENT3(0, "verbose", OPT_FLAG, NULL, &cmdlineP->verbose, 0); opt.opt_table = option_def; opt.short_allowed = false; /* We have no short (old-fashioned) options */ opt.allowNegNum = false; /* We have no parms that are negative numbers */ pm_optParseOptions3(&argc, (char **)argv, opt, sizeof(opt), 0); /* Uses and sets argc, argv, and some of *cmdlineP and others. */ if (!notefileSpec) cmdlineP->notefile = NULL; if (argc-1 < 1) cmdlineP->inputFileName = "-"; else if (argc-1 == 1) cmdlineP->inputFileName = argv[1]; else pm_error("Program takes at most one argument: input file name"); free(option_def); } #define getg16pixel(b,o) (((b) >> (4 - 4*(o))) & 0x0f) #define getgpixel(b,o) (((b) >> (6 - 2*(o))) & 0x03) #define getmpixel(b,o) (((b) >> (7 - (o))) & 0x01) static void abortShort() { pm_error("Invalid image. Compression algorithm runs out of " "compressed data before generating the expected " "amount of image data"); } static void abortOverrun() { pm_error("Invalid image. Compression algorithm finds the end of " "the image in the middle of a run"); } static void decompress(const uint8_t * const compressed, size_t const compressedSize, size_t const imageSize, uint8_t ** const uncompressedP) { /*---------------------------------------------------------------------------- Decompress the data 'compressed', which is 'compressedSize' bytes long. Return the decompressed data in newly malloced storage as *decompressedP. Decompression should yield exactly 'imageSize' bytes. -----------------------------------------------------------------------------*/ /* * The compression scheme used is a simple RLE; the control codes, * CODE, are one byte and have the following meanings: * * CODE > 0x80 Insert (CODE + 1 - 0x80) copies of the next byte. * CODE <= 0x80 Insert the next (CODE + 1) literal bytes. * * Compressed pieces can (and do) cross row boundaries. */ uint8_t * uncompressed; MALLOCARRAY(uncompressed, imageSize); if (uncompressed) { const uint8_t * inP; uint8_t * outP; size_t bytesLeft; for (bytesLeft = imageSize, inP = &compressed[0], outP = &uncompressed[0]; bytesLeft > 0; ) { int got, put; if (inP > compressed + compressedSize) abortShort(); if (*inP > 0x80) { put = *inP++ + 1 - 0x80; if (outP + put > uncompressed + imageSize) abortOverrun(); memset(outP, *inP, put); got = 1; } else { put = *inP++ + 1; if (inP + put > compressed + compressedSize) abortShort(); if (outP + put > uncompressed + imageSize) abortOverrun(); memcpy(outP, inP, put); got = put; } inP += got; outP += put; assert(bytesLeft >= put); bytesLeft -= put; } } *uncompressedP = uncompressed; } #define UNKNOWN_OFFSET (uint32_t)-1 static void readCompressed(IMAGE * const imgP, uint32_t const end_offset, FILE * const fP, size_t * const dataSizeP, uint8_t ** const dataP, int * const retvalP) { /*---------------------------------------------------------------------------- Read the compressed data from file *fP (actually, if the image isn't compressed, then it's just the regular data). Return the data in newly malloced storage as *dataP, which is *dataSizeP bytes long. -----------------------------------------------------------------------------*/ int retval; uint8_t * buffer; size_t dataSize; dataSize = 0; /* initial value */ if (end_offset == UNKNOWN_OFFSET) { /* * Read sufficient amount for worst-case compressed image, * or until EOF. Some of them have an extra zero byte * dangling off the end. I originally thought this was * an empty note record (even though there was no record * header for it); however, the release notes for Image * Compression Manager 1.1 on http://www.pilotgear.com * note this extra byte as a bug in Image Compression * Manager 1.0 which 1.1 fixes. We'll just blindly read * this extra byte and ignore it by paying attention to * the image dimensions. */ size_t const maxCompressedSizeWithBloat = ipdb_img_size(imgP) * 2; /* * Provide a buffer large enough for the worst case. * See note in lib/util/runlength.c . * We do not use pm_rlenc_allocoutbuf() because there is no * guarantee that the encoder that produced the image was * efficient. */ MALLOCARRAY(buffer, maxCompressedSizeWithBloat); if (buffer == NULL) retval = ENOMEM; else { dataSize = fread(buffer, 1, maxCompressedSizeWithBloat, fP); if (dataSize <= 0) retval = EIO; else retval = 0; if (retval != 0) free(buffer); } } else { /* * Read to the indicated offset. */ dataSize = end_offset - ftell(fP) + 1; MALLOCARRAY(buffer, dataSize); if (buffer == NULL) retval = ENOMEM; else { ssize_t rc; rc = fread(buffer, 1, dataSize, fP); if (rc != dataSize) retval = EIO; else retval = 0; if (retval != 0) free(buffer); } } *dataSizeP = dataSize; *dataP = buffer; *retvalP = retval; } static void imageReadHeader(FILE * const fileP, IMAGE * const imgP, bool const dump) { fread(&imgP->name, 1, 32, fileP); pm_readcharu(fileP, &imgP->version); pm_readcharu(fileP, &imgP->type); fread(&imgP->reserved1, 1, 4, fileP); fread(&imgP->note, 1, 4, fileP); pm_readbigshortu(fileP, &imgP->x_last); pm_readbigshortu(fileP, &imgP->y_last); fread(&imgP->reserved2, 1, 4, fileP); pm_readbigshortu(fileP, &imgP->x_anchor); pm_readbigshortu(fileP, &imgP->y_anchor); pm_readbigshortu(fileP, &imgP->width); pm_readbigshortu(fileP, &imgP->height); if (dump) { pm_message("PDB IMAGE header:"); pm_message(" Name: '%.*s'", (int)sizeof(imgP->name), imgP->name); pm_message(" Version: %02x", imgP->version); pm_message(" Type: %s", ipdb_typeName(imgP->type)); pm_message(" Note: %02x %02x %02x %02x", imgP->note[0], imgP->note[1], imgP->note[2], imgP->note[3]); pm_message(" X_last: %u", imgP->x_last); pm_message(" Y_last: %u", imgP->y_last); pm_message(" X_anchor: %u", imgP->x_anchor); pm_message(" Y_anchor: %u", imgP->y_anchor); pm_message(" Width: %u", imgP->width); pm_message(" Height: %u", imgP->height); pm_message("Pixels per byte: %u", ipdb_img_ppb(imgP)); pm_message("Image size: %lu bytes", (unsigned long)ipdb_img_size(imgP)); } } static int imageReadData(FILE * const fileP, IMAGE * const imgP, uint32_t const end_offset) { size_t const imageSize = ipdb_img_size(imgP); int retval; size_t dataSize; uint8_t * buffer; readCompressed(imgP, end_offset, fileP, &dataSize, &buffer, &retval); if (retval == 0) { /* * Compressed data can cross row boundaries so we decompress * the data here to avoid messiness in the row access functions. */ if (dataSize < imageSize || imgP->version == 1) { if (imgP->version == 0) pm_message("Image header says raster data is uncompressed. " "Encountered only %u instead of the " "required %u bytes. Assuming compressed mode.", (unsigned)dataSize, (unsigned)imageSize); decompress(buffer, dataSize, imageSize, &imgP->data); if (imgP->data == NULL) retval = ENOMEM; else imgP->compressed = true; free(buffer); } else { if (dataSize > imageSize) pm_message("Image header says raster data is uncompressed. " "Encountered %u instead of the required %u bytes. " "Assuming uncompressed mode.", (unsigned)dataSize, (unsigned)imageSize); imgP->compressed = false; imgP->data = buffer; /* Storage at 'buffer' now belongs to *imgP */ } } return retval; } static int imageRead(IMAGE * const imgP, uint32_t const end_offset, FILE * const fileP, bool const verbose) { if (imgP) { imgP->r->offset = (uint32_t)ftell(fileP); imageReadHeader(fileP, imgP, verbose); imageReadData(fileP, imgP, end_offset); } return 0; } static int textRead(TEXT * const textP, FILE * const fileP) { int retval; char * s; char buf[128]; int used, alloced, len; if (textP == NULL) return 0; textP->r->offset = (uint32_t)ftell(fileP); /* * What a pain in the ass! Why the hell isn't there a length * attached to the text record? I suppose the designer wasn't * concerned about non-seekable (i.e. pipes) input streams. * Perhaps I'm being a little harsh, the lack of a length probably * isn't much of an issue on the Pilot. */ used = 0; alloced = 0; s = NULL; retval = 0; /* initial value */ while ((len = fread(buf, 1, sizeof(buf), fileP)) != 0 && retval == 0) { if (buf[len - 1] == '\0') --len; if (used + len > alloced) { alloced += 2 * sizeof(buf); REALLOCARRAY(s, alloced); if (s == NULL) retval = ENOMEM; } if (retval == 0) { memcpy(s + used, buf, len); used += len; } } if (retval == 0) { textP->data = calloc(1, used + 1); if (textP->data == NULL) retval = ENOMEM; else memcpy(textP->data, s, used); } if (s) free(s); return retval; } static int pdbheadRead(PDBHEAD * const pdbHeadP, FILE * const fileP) { int retval; fread(pdbHeadP->name, 1, 32, fileP); pm_readbigshortu(fileP, &pdbHeadP->flags); pm_readbigshortu(fileP, &pdbHeadP->version); pm_readbiglongu2(fileP, &pdbHeadP->ctime); pm_readbiglongu2(fileP, &pdbHeadP->mtime); pm_readbiglongu2(fileP, &pdbHeadP->btime); pm_readbiglongu2(fileP, &pdbHeadP->mod_num); pm_readbiglongu2(fileP, &pdbHeadP->app_info); pm_readbiglongu2(fileP, &pdbHeadP->sort_info); fread(pdbHeadP->type, 1, 4, fileP); fread(pdbHeadP->id, 1, 4, fileP); pm_readbiglongu2(fileP, &pdbHeadP->uniq_seed); pm_readbiglongu2(fileP, &pdbHeadP->next_rec); pm_readbigshortu(fileP, &pdbHeadP->num_recs); if (!memeq(pdbHeadP->type, IPDB_vIMG, 4) || !memeq(pdbHeadP->id, IPDB_View, 4)) retval = E_NOTIMAGE; else retval = 0; return retval; } static int rechdrRead(RECHDR * const rechdrP, FILE * const fileP) { int retval; off_t len; pm_readbiglongu2(fileP, &rechdrP->offset); len = (off_t)rechdrP->offset - ftell(fileP); switch(len) { case 4: case 12: /* * Version zero (eight bytes of record header) or version * two with a note (two chunks of eight record header bytes). */ fread(&rechdrP->unknown[0], 1, 3, fileP); fread(&rechdrP->rec_type, 1, 1, fileP); rechdrP->n_extra = 0; rechdrP->extra = NULL; retval = 0; break; case 6: /* * Version one (ten bytes of record header). */ fread(&rechdrP->unknown[0], 1, 3, fileP); fread(&rechdrP->rec_type, 1, 1, fileP); rechdrP->n_extra = 2; MALLOCARRAY(rechdrP->extra, rechdrP->n_extra); if (rechdrP->extra == NULL) retval = ENOMEM; else { fread(rechdrP->extra, 1, rechdrP->n_extra, fileP); retval = 0; } break; default: /* * hmmm.... I'll assume this is the record header * for a text record. */ fread(&rechdrP->unknown[0], 1, 3, fileP); fread(&rechdrP->rec_type, 1, 1, fileP); rechdrP->n_extra = 0; rechdrP->extra = NULL; retval = 0; break; } if (retval == 0) { if ((rechdrP->rec_type != IMG_REC && rechdrP->rec_type != TEXT_REC) || !memeq(rechdrP->unknown, IPDB_MYST, 3)) retval = E_NOTRECHDR; } return retval; } static int ipdbRead(IPDB * const pdbP, FILE * const fileP, bool const verbose) { int retval; ipdb_clear(pdbP); pdbP->p = ipdb_pdbhead_alloc(NULL); if (pdbP->p == NULL) retval = ENOMEM; else { int status; status = pdbheadRead(pdbP->p, fileP); if (status != 0) retval = status; else { pdbP->i = ipdb_image_alloc(pdbP->p->name, IMG_GRAY, 0, 0); if (pdbP->i == NULL) retval = ENOMEM; else { int status; status = rechdrRead(pdbP->i->r, fileP); if (status != 0) retval = status; else { if (pdbP->p->num_recs > 1) { pdbP->t = ipdb_text_alloc(NULL); if (pdbP->t == NULL) retval = ENOMEM; else { int status; status = rechdrRead(pdbP->t->r, fileP); if (status != 0) retval = status; else retval = 0; } } else retval = 0; if (retval == 0) { uint32_t const offset = pdbP->t == NULL ? UNKNOWN_OFFSET : pdbP->t->r->offset - 1; int status; status = imageRead(pdbP->i, offset, fileP, verbose); if (status != 0) retval = status; else { if (pdbP->t != NULL) { int status; status = textRead(pdbP->t, fileP); if (status != 0) retval = status; } } } } } } } return retval; } static void g16unpack(const uint8_t * const p, uint8_t * const g, int const w) { static const uint8_t pal[] = {0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x00}; const uint8_t * seg; unsigned int i; for (i = 0, seg = p; i < w; i += 2, ++seg) { g[i + 0] = pal[getg16pixel(*seg, 0)]; g[i + 1] = pal[getg16pixel(*seg, 1)]; } } static void gunpack(const uint8_t * const p, uint8_t * const g, int const w) { static const uint8_t pal[] = {0xff, 0xaa, 0x55, 0x00}; const uint8_t * seg; unsigned int i; for (i = 0, seg = p; i < w; i += 4, ++seg) { g[i + 0] = pal[getgpixel(*seg, 0)]; g[i + 1] = pal[getgpixel(*seg, 1)]; g[i + 2] = pal[getgpixel(*seg, 2)]; g[i + 3] = pal[getgpixel(*seg, 3)]; } } static void munpack(const uint8_t * const p, uint8_t * const b, int const w) { static const uint8_t pal[] = {PAM_BW_WHITE, PAM_BLACK}; const uint8_t * seg; unsigned int i; for (i = 0, seg = p; i < w; i += 8, ++seg) { b[i + 0] = pal[getmpixel(*seg, 0)]; b[i + 1] = pal[getmpixel(*seg, 1)]; b[i + 2] = pal[getmpixel(*seg, 2)]; b[i + 3] = pal[getmpixel(*seg, 3)]; b[i + 4] = pal[getmpixel(*seg, 4)]; b[i + 5] = pal[getmpixel(*seg, 5)]; b[i + 6] = pal[getmpixel(*seg, 6)]; b[i + 7] = pal[getmpixel(*seg, 7)]; } } static void g16row(IPDB * const pdbP, unsigned int const row, uint8_t * const buffer) { g16unpack(ipdb_img_row(pdbP->i, row), buffer, ipdb_width(pdbP)); } static void grow(IPDB * const pdbP, unsigned int const row, uint8_t * const buffer) { gunpack(ipdb_img_row(pdbP->i, row), buffer, ipdb_width(pdbP)); } static void mrow(IPDB * const pdbP, unsigned int const row, uint8_t * const buffer) { munpack(ipdb_img_row(pdbP->i, row), buffer, ipdb_width(pdbP)); } static void writeImgPam(IPDB * const pdbP, FILE * const ofP) { struct pam pam; tuple * tupleRow; unsigned int row; uint8_t * imgRow; MALLOCARRAY(imgRow, ipdb_width(pdbP)); pam.size = sizeof(pam); pam.len = PAM_STRUCT_SIZE(tuple_type); pam.file = ofP; pam.plainformat = 0; pam.width = ipdb_width(pdbP); pam.height = ipdb_height(pdbP); pam.depth = 1; pam.maxval = ipdb_type(pdbP) == IMG_MONO ? 1 : 255; pam.bytes_per_sample = pnm_bytespersample(pam.maxval); pam.format = PAM_FORMAT; strcpy(pam.tuple_type, ipdb_type(pdbP) == IMG_MONO ? PAM_PBM_TUPLETYPE : PAM_PGM_TUPLETYPE); pnm_writepaminit(&pam); tupleRow = pnm_allocpamrow(&pam); for (row = 0; row < pam.height; ++row) { unsigned int col; if (ipdb_type(pdbP) == IMG_MONO) mrow(pdbP, row, imgRow); else if (ipdb_type(pdbP) == IMG_GRAY) grow(pdbP, row, imgRow); else g16row(pdbP, row, imgRow); for (col = 0; col < pam.width; ++col) tupleRow[col][0] = imgRow[col]; pnm_writepamrow(&pam, tupleRow); } pnm_freepamrow(tupleRow); free(imgRow); } static void writeText(IPDB * const pdbP, const char * const name) { const char * const note = ipdb_text(pdbP); FILE * fP; if (name == NULL || note == NULL) { } else { fP = pm_openw(name); if (fP == NULL) pm_error("Could not open note file '%s' for output", name); fprintf(fP, "%s\n", note); pm_close(fP); } } int main(int argc, const char ** argv) { struct cmdlineInfo cmdline; FILE * ifP; IPDB * pdbP; int status; pm_proginit(&argc, argv); parseCommandLine(argc, argv, &cmdline); ifP = pm_openr(cmdline.inputFileName); pdbP = ipdb_alloc(NULL); if (pdbP == NULL) pm_error("Could not allocate IPDB structure."); status = ipdbRead(pdbP, ifP, cmdline.verbose); if (status != 0) pm_error("Image header read error: %s.", ipdb_err(status)); writeImgPam(pdbP, stdout); writeText(pdbP, cmdline.notefile); ipdb_free(pdbP); pm_close(ifP); return EXIT_SUCCESS; }