123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596 |
- Program WrJpgCom;
- { wrjpgcom.c
- Copyright (C) 1994-1997, Thomas G. Lane.
- This file is part of the Independent JPEG Group's software.
- For conditions of distribution and use, see the accompanying README file.
- This file contains a very simple stand-alone application that inserts
- user-supplied text as a COM (comment) marker in a JFIF file.
- This may be useful as an example of the minimum logic needed to parse
- JPEG markers. }
- uses
- jmorecfg,
- jinclude,
- cdjpeg,
- strings,
- fcache;
- const
- EXIT_FAILURE = 1; { define Halt() codes if not provided }
- EXIT_SUCCESS = 0;
- { Reduce this value if your malloc() can't allocate blocks up to 64K.
- On DOS, compiling in large model is usually a better solution. }
- const
- MAX_COM_LENGTH = Long(32000); { must be <= 65533 in any case }
- { These macros are used to read the input file and write the output file.
- To reuse this code in another application, you might need to change these. }
- var
- infile : file; { input JPEG file }
- { Return next input byte, or EOF if no more }
- var
- outfile : file; { output JPEG file }
- { Emit an output byte }
- function NEXTBYTE : byte;
- var
- B : Byte;
- begin
- BlockRead(Infile,B,1);
- NEXTBYTE :=b;
- // fc_getc(var fc : Cache);
- { Read a byte at the current buffer read-index, increment the buffer
- read-index }
- end;
- procedure PUTBYTE(c : int);
- begin
- BlockWrite(outfile,c, 1);
- end;
- { Error exit handler }
- procedure ERREXIT(msg : string);
- begin
- WriteLn(msg);
- Halt(EXIT_FAILURE);
- end;
- { Read one byte, testing for EOF }
- function read_1_byte : int;
- var
- c : byte;
- begin
- c := NEXTBYTE;
- if (c = int(EOF)) then
- ERREXIT('Premature EOF in JPEG file');
- read_1_byte := c;
- end;
- { Read 2 bytes, convert to uint }
- { All 2-byte quantities in JPEG markers are MSB first }
- function read_2_bytes : uint;
- var
- c1, c2 : int;
- begin
- c1 := NEXTBYTE;
- if (c1 = int(EOF)) then
- ERREXIT('Premature EOF in JPEG file');
- c2 := NEXTBYTE;
- if (c2 = int(EOF)) then
- ERREXIT('Premature EOF in JPEG file');
- read_2_bytes := ((uint(c1)) shl 8) + (uint(c2));
- end;
- { Routines to write data to output file }
- procedure write_1_byte (c : int);
- begin
- PUTBYTE(c);
- end;
- procedure write_2_bytes (val : uint);
- begin
- PUTBYTE((val shr 8) and $FF);
- PUTBYTE(val and $FF);
- end;
- procedure write_marker (marker : int);
- begin
- PUTBYTE($FF);
- PUTBYTE(marker);
- end;
- procedure copy_rest_of_file;
- var
- c : int;
- begin
- repeat
- c := NEXTBYTE;
- if (c <> int(EOF)) then
- PUTBYTE(c);
- until (c = int(EOF));
- end;
- { JPEG markers consist of one or more $FF bytes, followed by a marker
- code byte (which is not an FF). Here are the marker codes of interest
- in this program. (See jdmarker.c for a more complete list.) }
- const
- M_SOF0 = $C0; { Start Of Frame N }
- M_SOF1 = $C1; { N indicates which compression process }
- M_SOF2 = $C2; { Only SOF0-SOF2 are now in common use }
- M_SOF3 = $C3;
- M_SOF5 = $C5; { NB: codes C4 and CC are NOT SOF markers }
- M_SOF6 = $C6;
- M_SOF7 = $C7;
- M_SOF9 = $C9;
- M_SOF10 = $CA;
- M_SOF11 = $CB;
- M_SOF13 = $CD;
- M_SOF14 = $CE;
- M_SOF15 = $CF;
- M_SOI = $D8; { Start Of Image (beginning of datastream) }
- M_EOI = $D9; { End Of Image (end of datastream) }
- M_SOS = $DA; { Start Of Scan (begins compressed data) }
- M_COM = $FE; { COMment }
- { Find the next JPEG marker and return its marker code.
- We expect at least one FF byte, possibly more if the compressor used FFs
- to pad the file. (Padding FFs will NOT be replicated in the output file.)
- There could also be non-FF garbage between markers. The treatment of such
- garbage is unspecified; we choose to skip over it but emit a warning msg.
- NB: this routine must not be used after seeing SOS marker, since it will
- not deal correctly with FF/00 sequences in the compressed image data... }
- function next_marker : int;
- var
- c : int;
- discarded_bytes : int;
- begin
- discarded_bytes := 0;
- { Find $FF byte; count and skip any non-FFs. }
- c := read_1_byte;
- while (c <> $FF) do
- begin
- Inc(discarded_bytes);
- c := read_1_byte;
- end;
- { Get marker code byte, swallowing any duplicate FF bytes. Extra FFs
- are legal as pad bytes, so don't count them in discarded_bytes. }
- repeat
- c := read_1_byte;
- until (c <> $FF);
- if (discarded_bytes <> 0) then
- begin
- WriteLn('Warning: garbage data found in JPEG file');
- end;
- next_marker := c;
- end;
- { Read the initial marker, which should be SOI.
- For a JFIF file, the first two bytes of the file should be literally
- $FF M_SOI. To be more general, we could use next_marker, but if the
- input file weren't actually JPEG at all, next_marker might read the whole
- file and then return a misleading error message... }
- function first_marker : int;
- var
- c1, c2 : int;
- begin
- c1 := NEXTBYTE;
- c2 := NEXTBYTE;
- if (c1 <> $FF) or (c2 <> M_SOI) then
- ERREXIT('Not a JPEG file');
- first_marker := c2;
- end;
- { Most types of marker are followed by a variable-length parameter segment.
- This routine skips over the parameters for any marker we don't otherwise
- want to process.
- Note that we MUST skip the parameter segment explicitly in order not to
- be fooled by $FF bytes that might appear within the parameter segment;
- such bytes do NOT introduce new markers. }
- procedure copy_variable;
- { Copy an unknown or uninteresting variable-length marker }
- var
- length : uint;
- begin
- { Get the marker parameter length count }
- length := read_2_bytes;
- write_2_bytes(length);
- { Length includes itself, so must be at least 2 }
- if (length < 2) then
- ERREXIT('Erroneous JPEG marker length');
- Dec(length, 2);
- { Skip over the remaining bytes }
- while (length > 0) do
- begin
- write_1_byte(read_1_byte);
- Dec(length);
- end;
- end;
- procedure skip_variable;
- { Skip over an unknown or uninteresting variable-length marker }
- var
- length : uint;
- begin
- { Get the marker parameter length count }
- length := read_2_bytes;
- { Length includes itself, so must be at least 2 }
- if (length < 2) then
- ERREXIT('Erroneous JPEG marker length');
- Dec(length, 2);
- { Skip over the remaining bytes }
- while (length > 0) do
- begin
- read_1_byte;
- Dec(length);
- end;
- end;
- { Parse the marker stream until SOFn or EOI is seen;
- copy data to output, but discard COM markers unless keep_COM is true. }
- function scan_JPEG_header (keep_COM : boolean) : int;
- var
- marker : int;
- begin
- { Expect SOI at start of file }
- if (first_marker <> M_SOI) then
- ERREXIT('Expected SOI marker first');
- write_marker(M_SOI);
- { Scan miscellaneous markers until we reach SOFn. }
- while TRUE do
- begin
- marker := next_marker;
- case marker of
- { Note that marker codes $C4, $C8, $CC are not, and must not be,
- treated as SOFn. C4 in particular is actually DHT. }
- M_SOF0, { Baseline }
- M_SOF1, { Extended sequential, Huffman }
- M_SOF2, { Progressive, Huffman }
- M_SOF3, { Lossless, Huffman }
- M_SOF5, { Differential sequential, Huffman }
- M_SOF6, { Differential progressive, Huffman }
- M_SOF7, { Differential lossless, Huffman }
- M_SOF9, { Extended sequential, arithmetic }
- M_SOF10, { Progressive, arithmetic }
- M_SOF11, { Lossless, arithmetic }
- M_SOF13, { Differential sequential, arithmetic }
- M_SOF14, { Differential progressive, arithmetic }
- M_SOF15: { Differential lossless, arithmetic }
- begin
- scan_JPEG_header := marker;
- exit;
- end;
- M_SOS: { should not see compressed data before SOF }
- ERREXIT('SOS without prior SOFn');
- M_EOI: { in case it's a tables-only JPEG stream }
- begin
- scan_JPEG_header := marker;
- exit;
- end;
- M_COM: { Existing COM: conditionally discard }
- if (keep_COM) then
- begin
- write_marker(marker);
- copy_variable;
- end
- else
- begin
- skip_variable;
- end;
- else { Anything else just gets copied }
- write_marker(marker);
- copy_variable; { we assume it has a parameter count... }
- end;
- end; { end loop }
- end;
- { Command line parsing code }
- var
- progname : string; { program name for error messages }
- procedure usage;
- { complain about bad command line }
- begin
- WriteLn('wrjpgcom inserts a textual comment in a JPEG file.');
- WriteLn('You can add to or replace any existing comment(s).');
- Write('Usage: ',progname,' [switches] ');
- {$ifdef TWO_FILE_COMMANDLINE
- WriteLn('inputfile outputfile');
- {$else}
- WriteLn('[inputfile]');
- {$endif}
- WriteLn('Switches (names may be abbreviated):');
- WriteLn(' -replace Delete any existing comments');
- WriteLn(' -comment "text" Insert comment with given text');
- WriteLn(' -cfile name Read comment from named file');
- WriteLn('Notice that you must put quotes around the comment text');
- WriteLn('when you use -comment.');
- WriteLn('If you do not give either -comment or -cfile on the command line,');
- WriteLn('then the comment text is read from standard input.');
- WriteLn('It can be multiple lines, up to ',
- uint(MAX_COM_LENGTH),' characters total.');
- {$ifndef TWO_FILE_COMMANDLINE}
- WriteLn('You must specify an input JPEG file name when supplying');
- WriteLn('comment text from standard input.');
- {$endif}
- Halt(EXIT_FAILURE);
- end;
- function keymatch (const arg : string;
- const keyword : string;
- minchars : int) : boolean;
- { Case-insensitive matching of (possibly abbreviated) keyword switches. }
- { keyword is the constant keyword (must be lower case already), }
- { minchars is length of minimum legal abbreviation. }
- var
- {register} ca, ck : char;
- {register} nmatched : int;
- i, len : int;
- begin
- nmatched := 0;
- keymatch := FALSE;
- len := Length(keyword);
- if len >= Length(arg) then
- len := Length(arg)
- else
- exit;
- for i := 1 to len do
- begin
- if (UpCase(arg[i]) <> UpCase(keyword[i])) then
- exit;
- Inc(nmatched); { count matched characters }
- end;
- { reached end of argument; fail if it's too short for unique abbrev }
- if (nmatched >= minchars) then
- keymatch := TRUE; { A-OK }
- end;
- { The main program. }
- var
- argc,
- argn : int;
- arg : string;
- keep_COM : boolean;
- comment_arg : string;
- comment_arg_0 : PChar;
- comment_file : TBufStream;
- comment_length : uint;
- marker : int;
- var
- src_file : PBufStream;
- c : int;
- begin
- keep_COM := TRUE;
- comment_arg := '';
- comment_length := 0;
- { On Mac, fetch a command line. }
- argc := ParamCount;
- progname := ParamStr(0);
- { Parse switches, if any }
- argn := 1;
- while (argn < argc) do
- begin
- arg := ParamStr(argn);
- if (arg[1] <> '-') then
- break; { not switch, must be file name }
- if (keymatch(arg, '-replace', 2)) then
- begin
- keep_COM := FALSE;
- end
- else
- if (keymatch(arg, '-cfile', 3)) then
- begin
- Inc(argn);
- if (argn >= argc) then
- usage;
- if not comment_file.Init(ParamStr(argn), stOpenRead, 2048) then
- begin
- WriteLn(progname, 'can''t open ', ParamStr(argn));
- Halt(EXIT_FAILURE);
- end;
- end
- else
- {$ifdef comment}
- if (keymatch(arg, '-comment', 2)) then
- begin
- Inc(argn);
- if (argn >= argc) then
- usage;
- comment_arg := ParamStr(argn);
- { If the comment text starts with '"', then we are probably running
- under MS-DOG and must parse out the quoted string ourselves. Sigh. }
- if (comment_arg[1] = '"') then
- begin
- GetMem(comment_arg_0, size_t(MAX_COM_LENGTH) );
- if (comment_arg_0 = NIL) then
- ERREXIT('Insufficient memory');
- strcopy(comment_arg_0, ParamStr(argn)+1);
- while TRUE do
- begin
- comment_length := uint( strlen(comment_arg) );
- if (comment_length > 0) and
- (comment_arg[comment_length-1] = '"') then
- begin
- comment_arg[comment_length-1] := #0; { zap terminating quote }
- break;
- end;
- Inc(argn);
- if (argn >= argc) then
- ERREXIT('Missing ending quote mark');
- strcat(comment_arg, ' ');
- strcat(comment_arg, argv[argn]);
- end;
- end;
- comment_length := uint(strlen(comment_arg));
- end
- else
- usage;
- {$endif}
- Inc(argn);
- end;
- { Cannot use both -comment and -cfile. }
- if (comment_arg <> '') and (comment_file.status <> stOK) then
- usage;
- { If there is neither -comment nor -cfile, we will read the comment text
- from stdin; in this case there MUST be an input JPEG file name. }
- if (comment_arg = '') and (comment_file.status <> stOK) and (argn >= argc) then
- usage;
- { Open the input file. }
- if (argn < argc) then
- begin
- infile.Init(ParamStr(argn), stOpenRead, 2048);
- if (infile.Status <> stOK) then
- begin
- WriteLn(progname, ': can''t open ', ParamStr(argn));
- Halt(EXIT_FAILURE);
- end;
- end
- else
- begin
- { default input file is stdin }
- {$ifdef USE_FDOPEN} { need to re-open in binary mode? }
- infile := TBufStream.Init('', stOpenRead, 2048);
- if (infile.Status <> stOK) then
- begin
- WriteLn(progname, ': can''t open stdin');
- Halt(EXIT_FAILURE);
- end;
- {$else}
- {infile := input;}
- RunError(255);
- {$endif}
- end;
- { Open the output file. }
- {$ifdef TWO_FILE_COMMANDLINE}
- { Must have explicit output file name }
- if (argn <> argc-2) then
- begin
- WriteLn(progname, ': must name one input and one output file');
- usage;
- end;
- outfile := TBufStream.Init(ParamStr(argn+1), stOpenWrite, 2048);
- if (outfile.Status <> stOK) then
- begin
- WriteLn(progname, ': can't open ', ParamStr(argn+1));
- Halt(EXIT_FAILURE);
- end;
- {$else}
- { Unix style: expect zero or one file name }
- if (argn < argc-1) then
- begin
- WriteLn(progname, ': only one input file');
- usage;
- end;
- { default output file is stdout }
- {$ifdef USE_FDOPEN} { need to re-open in binary mode? }
- outfile := TBufStream.Init('', stOpenWrite, 2048);
- if (outfile.Status <> stOK) then
- begin
- WriteLn(progname, ': can''t open stdout');
- Halt(EXIT_FAILURE);
- end;
- {$else}
- RunError(255);
- {outfile := stdout;}
- {$endif}
- {$endif} { TWO_FILE_COMMANDLINE }
- { Collect comment text from comment_file or stdin, if necessary }
- if (comment_arg_0 = NIL) then
- begin
- GetMem(comment_arg_0, size_t(MAX_COM_LENGTH) );
- if (comment_arg_0 = NIL) then
- ERREXIT('Insufficient memory');
- comment_length := 0;
- if comment_file.status = stOK then
- src_file := @comment_file
- else
- src_file := NIL;
- repeat
- c := getc(src_file);
- if (c <> EOF) do
- begin
- if (comment_length >= uint(MAX_COM_LENGTH)) then
- begin
- WriteLn('Comment text may not exceed ',
- uint(MAX_COM_LENGTH)),' bytes);
- Halt(EXIT_FAILURE);
- end;
- comment_arg[comment_length] := char(c);
- Inc(comment_length);
- end;
- until (c = EOF);
- if (comment_file <> '') then
- fclose(comment_file);
- end;
- { Copy JPEG headers until SOFn marker;
- we will insert the new comment marker just before SOFn.
- This (a) causes the new comment to appear after, rather than before,
- existing comments; and (b) ensures that comments come after any JFIF
- or JFXX markers, as required by the JFIF specification. }
- marker := scan_JPEG_header(keep_COM);
- { Insert the new COM marker, but only if nonempty text has been supplied }
- if (comment_length > 0) then
- begin
- write_marker(M_COM);
- write_2_bytes(comment_length + 2);
- while (comment_length > 0) do
- begin
- write_1_byte(comment_arg^);
- Inc(comment_arg);
- Dec(comment_length);
- end;
- end;
- { Duplicate the remainder of the source file.
- Note that any COM markers occuring after SOF will not be touched. }
- write_marker(marker);
- copy_rest_of_file();
- { All done. }
- Halt(EXIT_SUCCESS);
- end.
|