//-------------------------------------------------------------------------- // Add a section (assume it doesn't already exist) - used for // adding comment sections. //-------------------------------------------------------------------------- Section_t * CreateSection(int SectionType, unsigned char * Data, int Size) { Section_t * NewSection; int a; // Insert it in third position - seems like a safe place to put // things like comments. if (SectionsRead < 2){ ErrFatal("Too few sections!"); } if (SectionsRead >= MAX_SECTIONS){ ErrFatal("Too many sections!"); } for (a=SectionsRead;a>2;a--){ Sections[a] = Sections[a-1]; } SectionsRead += 1; NewSection = Sections+2; NewSection->Type = SectionType; NewSection->Size = Size; NewSection->Data = Data; return NewSection; }
//-------------------------------------------------------------------------- // Write image data back to disk. //-------------------------------------------------------------------------- void WriteJpegFile(const char * FileName) { FILE * outfile; int a; if (!HaveAll){ ErrFatal("Can't write back - didn't read all"); } outfile = fopen(FileName,"wb"); if (outfile == NULL){ ErrFatal("Could not open file for write"); } // Initial static jpeg marker. fputc(0xff,outfile); fputc(0xd8,outfile); if (Sections[0].Type != M_EXIF && Sections[0].Type != M_JFIF){ // The image must start with an exif or jfif marker. If we threw those away, create one. static uchar JfifHead[18] = { 0xff, M_JFIF, 0x00, 0x10, 'J' , 'F' , 'I' , 'F' , 0x00, 0x01, 0x01, 0x01, 0x01, 0x2C, 0x01, 0x2C, 0x00, 0x00 }; if (ImageInfo.ResolutionUnit == 2 || ImageInfo.ResolutionUnit == 3){ // Use the exif resolution info to fill out the jfif header. // Usually, for exif images, there's no jfif header, so if wediscard // the exif header, use info from the exif header for the jfif header. ImageInfo.JfifHeader.ResolutionUnits = (char)(ImageInfo.ResolutionUnit-1); // Jfif is 1 and 2, Exif is 2 and 3 for In and cm respecively ImageInfo.JfifHeader.XDensity = (int)ImageInfo.xResolution; ImageInfo.JfifHeader.YDensity = (int)ImageInfo.yResolution; } JfifHead[11] = ImageInfo.JfifHeader.ResolutionUnits; JfifHead[12] = (uchar)(ImageInfo.JfifHeader.XDensity >> 8); JfifHead[13] = (uchar)ImageInfo.JfifHeader.XDensity; JfifHead[14] = (uchar)(ImageInfo.JfifHeader.YDensity >> 8); JfifHead[15] = (uchar)ImageInfo.JfifHeader.YDensity; fwrite(JfifHead, 18, 1, outfile); // use the values from the exif data for the jfif header, if we have found values if (ImageInfo.ResolutionUnit != 0) { // JFIF.ResolutionUnit is {1,2}, EXIF.ResolutionUnit is {2,3} JfifHead[11] = (uchar)ImageInfo.ResolutionUnit - 1; } if (ImageInfo.xResolution > 0.0 && ImageInfo.yResolution > 0.0) { JfifHead[12] = (uchar)((int)ImageInfo.xResolution>>8); JfifHead[13] = (uchar)((int)ImageInfo.xResolution); JfifHead[14] = (uchar)((int)ImageInfo.yResolution>>8); JfifHead[15] = (uchar)((int)ImageInfo.yResolution); } }
//-------------------------------------------------------------------------- // Replace or remove exif thumbnail //-------------------------------------------------------------------------- int ReplaceThumbnail(const char * ThumbFileName) { FILE * ThumbnailFile; int ThumbLen, NewExifSize; Section_t * ExifSection; uchar * ThumbnailPointer; if (ImageInfo.ThumbnailOffset == 0 || ImageInfo.ThumbnailAtEnd == FALSE){ // Adding or removing of thumbnail is not possible - that would require rearranging // of the exif header, which is risky, and jhad doesn't know how to do. printf("Image contains no thumbnail to replace - add is not possible\n"); return FALSE; } if (ThumbFileName){ ThumbnailFile = fopen(ThumbFileName,"rb"); if (ThumbnailFile == NULL){ ErrFatal("Could not read thumbnail file"); return FALSE; } // get length fseek(ThumbnailFile, 0, SEEK_END); ThumbLen = ftell(ThumbnailFile); fseek(ThumbnailFile, 0, SEEK_SET); if (ThumbLen + ImageInfo.ThumbnailOffset > 0x10000-20){ ErrFatal("Thumbnail is too large to insert into exif header"); } }else{ ThumbLen = 0; ThumbnailFile = NULL; } ExifSection = FindSection(M_EXIF); NewExifSize = ImageInfo.ThumbnailOffset+8+ThumbLen; ExifSection->Data = (uchar *)realloc(ExifSection->Data, NewExifSize); ThumbnailPointer = ExifSection->Data+ImageInfo.ThumbnailOffset+8; if (ThumbnailFile){ fread(ThumbnailPointer, ThumbLen, 1, ThumbnailFile); fclose(ThumbnailFile); } ImageInfo.ThumbnailSize = ThumbLen; Put32u(ExifSection->Data+ImageInfo.ThumbnailSizeOffset+8, ThumbLen); ExifSection->Data[0] = (uchar)(NewExifSize >> 8); ExifSection->Data[1] = (uchar)NewExifSize; ExifSection->Size = NewExifSize; return TRUE; }
//-------------------------------------------------------------------------- // Check sections array to see if it needs to be increased in size. //-------------------------------------------------------------------------- void CheckSectionsAllocated(void) { if (SectionsRead > SectionsAllocated){ ErrFatal("allocation screwup"); } if (SectionsRead >= SectionsAllocated){ SectionsAllocated += SectionsAllocated/2; Sections = (Section_t *)realloc(Sections, sizeof(Section_t)*SectionsAllocated); if (Sections == NULL){ ErrFatal("could not allocate data for entire image"); } } }
//-------------------------------------------------------------------------- // Invoke an editor for editing a sting. //-------------------------------------------------------------------------- static int FileEditComment(char * TempFileName, char * Comment, int CommentSize) { FILE * file; int a; char QuotedPath[300]; file = fopen(TempFileName, "w"); if (file == NULL){ fprintf(stderr, "Can't create file '%s'\n",TempFileName); ErrFatal("could not create temporary file"); } fwrite(Comment, CommentSize, 1, file); fclose(file); fflush(stdout); // So logs are contiguous. { char * Editor; Editor = getenv("EDITOR"); if (Editor == NULL){ #ifdef _WIN32 Editor = "notepad"; #else Editor = "vi"; #endif } sprintf(QuotedPath, "%s \"%s\"",Editor, TempFileName); a = system(QuotedPath); } if (a != 0){ perror("Editor failed to launch"); exit(-1); } file = fopen(TempFileName, "r"); if (file == NULL){ ErrFatal("could not open temp file for read"); } // Read the file back in. CommentSize = fread(Comment, 1, 999, file); fclose(file); unlink(TempFileName); return CommentSize; }
//-------------------------------------------------------------------------- // Replace or remove exif thumbnail //-------------------------------------------------------------------------- int SaveThumbnail(char * ThumbFileName) { FILE * ThumbnailFile; if (ImageInfo.ThumbnailOffset == 0 || ImageInfo.ThumbnailSize == 0){ fprintf(stderr,"Image contains no thumbnail\n"); return FALSE; } if (strcmp(ThumbFileName, "-") == 0){ // A filename of '-' indicates thumbnail goes to stdout. // This doesn't make much sense under Windows, so this feature is unix only. ThumbnailFile = stdout; }else{ ThumbnailFile = fopen(ThumbFileName,"wb"); } if (ThumbnailFile){ uchar * ThumbnailPointer; Section_t * ExifSection; ExifSection = FindSection(M_EXIF); ThumbnailPointer = ExifSection->Data+ImageInfo.ThumbnailOffset+8; fwrite(ThumbnailPointer, ImageInfo.ThumbnailSize ,1, ThumbnailFile); fclose(ThumbnailFile); return TRUE; }else{ ErrFatal("Could not write thumbnail file"); return FALSE; } }
//-------------------------------------------------------------------------- // Write image data back to disk. //-------------------------------------------------------------------------- void WriteJpegFile(const char * FileName) { FILE * outfile; int a; if (!HaveAll){ ErrFatal("Can't write back - didn't read all"); } outfile = fopen(FileName,"wb"); if (outfile == NULL){ ErrFatal("Could not open file for write"); } // Initial static jpeg marker. fputc(0xff,outfile); fputc(0xd8,outfile); if (Sections[0].Type != M_EXIF && Sections[0].Type != M_JFIF){ // The image must start with an exif or jfif marker. If we threw those away, create one. static uchar JfifHead[18] = { 0xff, M_JFIF, 0x00, 0x10, 'J' , 'F' , 'I' , 'F' , 0x00, 0x01, 0x01, 0x01, 0x01, 0x2C, 0x01, 0x2C, 0x00, 0x00 }; fwrite(JfifHead, 18, 1, outfile); } // Write all the misc sections for (a=0;a<SectionsRead-1;a++){ fputc(0xff,outfile); fputc(Sections[a].Type, outfile); fwrite(Sections[a].Data, Sections[a].Size, 1, outfile); } // Write the remaining image data. fwrite(Sections[a].Data, Sections[a].Size, 1, outfile); fclose(outfile); }
//-------------------------------------------------------------------------- // Parse the marker stream until SOS or EOI is seen; //-------------------------------------------------------------------------- int ReadJpegSections (FILE * infile, ReadMode_t ReadMode) { int a; int HaveCom = FALSE; a = fgetc(infile); if (a != 0xff || fgetc(infile) != M_SOI){ return FALSE; } ImageInfo.JfifHeader.XDensity = ImageInfo.JfifHeader.YDensity = 300; ImageInfo.JfifHeader.ResolutionUnits = 1; for(;;){ int itemlen; int prev; int marker = 0; int ll,lh, got; uchar * Data; CheckSectionsAllocated(); prev = 0; for (a=0;;a++){ marker = fgetc(infile); if (marker != 0xff && prev == 0xff) break; prev = marker; } if (a > 10){ ErrNonfatal("Extraneous %d padding bytes before section %02X",a-1,marker); } Sections[SectionsRead].Type = marker; // Read the length of the section. lh = fgetc(infile); ll = fgetc(infile); itemlen = (lh << 8) | ll; if (itemlen < 2){ ErrFatal("invalid marker"); } Sections[SectionsRead].Size = itemlen; Data = (uchar *)malloc(itemlen); if (Data == NULL){ ErrFatal("Could not allocate memory"); } Sections[SectionsRead].Data = Data; // Store first two pre-read bytes. Data[0] = (uchar)lh; Data[1] = (uchar)ll; got = fread(Data+2, 1, itemlen-2, infile); // Read the whole section. if (got != itemlen-2){ ErrFatal("Premature end of file?"); } SectionsRead += 1; switch(marker){ case M_SOS: // stop before hitting compressed data // If reading entire image is requested, read the rest of the data. if (ReadMode & READ_IMAGE){ int cp, ep, size; // Determine how much file is left. cp = ftell(infile); fseek(infile, 0, SEEK_END); ep = ftell(infile); fseek(infile, cp, SEEK_SET); size = ep-cp; Data = (uchar *)malloc(size); if (Data == NULL){ ErrFatal("could not allocate data for entire image"); } got = fread(Data, 1, size, infile); if (got != size){ ErrFatal("could not read the rest of the image"); } CheckSectionsAllocated(); Sections[SectionsRead].Data = Data; Sections[SectionsRead].Size = size; Sections[SectionsRead].Type = PSEUDO_IMAGE_MARKER; SectionsRead ++; HaveAll = 1; } return TRUE; case M_EOI: // in case it's a tables-only JPEG stream fprintf(stderr,"No image in jpeg!\n"); return FALSE; case M_COM: // Comment section if (HaveCom || ((ReadMode & READ_METADATA) == 0)){ // Discard this section. free(Sections[--SectionsRead].Data); }else{ process_COM(Data, itemlen); HaveCom = TRUE; } break; case M_JFIF: // Regular jpegs always have this tag, exif images have the exif // marker instead, althogh ACDsee will write images with both markers. // this program will re-create this marker on absence of exif marker. // hence no need to keep the copy from the file. if (memcmp(Data+2, "JFIF\0",5)){ fprintf(stderr,"Header missing JFIF marker\n"); } if (itemlen < 16){ fprintf(stderr,"Jfif header too short\n"); goto ignore; } ImageInfo.JfifHeader.Present = TRUE; ImageInfo.JfifHeader.ResolutionUnits = Data[9]; ImageInfo.JfifHeader.XDensity = (Data[10]<<8) | Data[11]; ImageInfo.JfifHeader.YDensity = (Data[12]<<8) | Data[13]; if (ShowTags){ printf("JFIF SOI marker: Units: %d ",ImageInfo.JfifHeader.ResolutionUnits); switch(ImageInfo.JfifHeader.ResolutionUnits){ case 0: printf("(aspect ratio)"); break; case 1: printf("(dots per inch)"); break; case 2: printf("(dots per cm)"); break; default: printf("(unknown)"); break; } printf(" X-density=%d Y-density=%d\n",ImageInfo.JfifHeader.XDensity, ImageInfo.JfifHeader.YDensity); if (Data[14] || Data[15]){ fprintf(stderr,"Ignoring jfif header thumbnail\n"); } } ignore: free(Sections[--SectionsRead].Data); break; case M_EXIF: // There can be different section using the same marker. if (ReadMode & READ_METADATA){ if (memcmp(Data+2, "Exif", 4) == 0){ process_EXIF(Data, itemlen); break; }else if (memcmp(Data+2, "http:", 5) == 0){ Sections[SectionsRead-1].Type = M_XMP; // Change tag for internal purposes. if (ShowTags){ printf("Image cotains XMP section, %d bytes long\n", itemlen); if (ShowTags){ ShowXmp(Sections[SectionsRead-1]); } } break; } } // Oterwise, discard this section. free(Sections[--SectionsRead].Data); break; case M_IPTC: if (ReadMode & READ_METADATA){ if (ShowTags){ printf("Image cotains IPTC section, %d bytes long\n", itemlen); } // Note: We just store the IPTC section. Its relatively straightforward // and we don't act on any part of it, so just display it at parse time. }else{ free(Sections[--SectionsRead].Data); } break; case M_SOF0: case M_SOF1: case M_SOF2: case M_SOF3: case M_SOF5: case M_SOF6: case M_SOF7: case M_SOF9: case M_SOF10: case M_SOF11: case M_SOF13: case M_SOF14: case M_SOF15: process_SOFn(Data, marker); break; default: // Skip any other sections. if (ShowTags){ printf("Jpeg section marker 0x%02x size %d\n",marker, itemlen); } break; } } return TRUE; }
//-------------------------------------------------------------------------- // Parse the marker stream until SOS or EOI is seen; //-------------------------------------------------------------------------- int ReadJpegSections (FILE * infile, ReadMode_t ReadMode) { int a; int HaveCom = FALSE; a = fgetc(infile); if (a != 0xff || fgetc(infile) != M_SOI){ return FALSE; } for(;;){ int itemlen; int marker = 0; int ll,lh, got; uchar * Data; if (SectionsRead >= MAX_SECTIONS){ ErrFatal("Too many sections in jpg file"); } for (a=0;a<7;a++){ marker = fgetc(infile); if (marker != 0xff) break; if (a >= 6){ printf("too many padding bytes\n"); return FALSE; } } if (marker == 0xff){ // 0xff is legal padding, but if we get that many, something's wrong. ErrFatal("too many padding bytes!"); } Sections[SectionsRead].Type = marker; // Read the length of the section. lh = fgetc(infile); ll = fgetc(infile); itemlen = (lh << 8) | ll; if (itemlen < 2){ ErrFatal("invalid marker"); } Sections[SectionsRead].Size = itemlen; Data = (uchar *)malloc(itemlen); if (Data == NULL){ ErrFatal("Could not allocate memory"); } Sections[SectionsRead].Data = Data; // Store first two pre-read bytes. Data[0] = (uchar)lh; Data[1] = (uchar)ll; got = fread(Data+2, 1, itemlen-2, infile); // Read the whole section. if (got != itemlen-2){ ErrFatal("Premature end of file?"); } SectionsRead += 1; switch(marker){ case M_SOS: // stop before hitting compressed data // If reading entire image is requested, read the rest of the data. if (ReadMode & READ_IMAGE){ int cp, ep, size; // Determine how much file is left. cp = ftell(infile); fseek(infile, 0, SEEK_END); ep = ftell(infile); fseek(infile, cp, SEEK_SET); size = ep-cp; Data = (uchar *)malloc(size); if (Data == NULL){ ErrFatal("could not allocate data for entire image"); } got = fread(Data, 1, size, infile); if (got != size){ ErrFatal("could not read the rest of the image"); } Sections[SectionsRead].Data = Data; Sections[SectionsRead].Size = size; Sections[SectionsRead].Type = PSEUDO_IMAGE_MARKER; SectionsRead ++; HaveAll = 1; } return TRUE; case M_EOI: // in case it's a tables-only JPEG stream printf("No image in jpeg!\n"); return FALSE; case M_COM: // Comment section if (HaveCom || ((ReadMode & READ_EXIF) == 0)){ // Discard this section. free(Sections[--SectionsRead].Data); }else{ process_COM(Data, itemlen); HaveCom = TRUE; } break; case M_JFIF: // Regular jpegs always have this tag, exif images have the exif // marker instead, althogh ACDsee will write images with both markers. // this program will re-create this marker on absence of exif marker. // hence no need to keep the copy from the file. free(Sections[--SectionsRead].Data); break; case M_EXIF: // Seen files from some 'U-lead' software with Vivitar scanner // that uses marker 31 for non exif stuff. Thus make sure // it says 'Exif' in the section before treating it as exif. if ((ReadMode & READ_EXIF) && memcmp(Data+2, "Exif", 4) == 0){ process_EXIF(Data, itemlen); }else{ // Discard this section. free(Sections[--SectionsRead].Data); } break; case M_SOF0: case M_SOF1: case M_SOF2: case M_SOF3: case M_SOF5: case M_SOF6: case M_SOF7: case M_SOF9: case M_SOF10: case M_SOF11: case M_SOF13: case M_SOF14: case M_SOF15: process_SOFn(Data, marker); break; default: // Skip any other sections. if (ShowTags){ printf("Jpeg section marker 0x%02x size %d\n",marker, itemlen); } break; } } return TRUE; }
//-------------------------------------------------------------------------- // Do selected operations to one file at a time. //-------------------------------------------------------------------------- void ProcessFile(const char * FileName) { int Modified = FALSE; ReadMode_t ReadMode = READ_EXIF; CurrentFile = FileName; ResetJpgfile(); // Start with an empty image information structure. memset(&ImageInfo, 0, sizeof(ImageInfo)); ImageInfo.FlashUsed = -1; ImageInfo.MeteringMode = -1; // Store file date/time. { struct stat st; if (stat(FileName, &st) >= 0){ ImageInfo.FileDateTime = st.st_mtime; ImageInfo.FileSize = st.st_size; }else{ ErrFatal("No such file"); } } strncpy(ImageInfo.FileName, FileName, PATH_MAX); if (ApplyCommand){ // Applying a command is special - the headers from the file have to be // pre-read, then the command executed, and then the image part of the file read. if (!ReadJpegFile(FileName, READ_EXIF)) return; #ifdef MATTHIAS if (AutoResize){ // Automatic resize computation - to customize for each run... if (AutoResizeCmdStuff() == 0){ DiscardData(); return; } } #endif // MATTHIAS if (CheckFileSkip()){ DiscardData(); return; } DiscardAllButExif(); DoCommand(FileName); Modified = TRUE; ReadMode = READ_IMAGE; // Don't re-read exif section again on next read. }else if (ExifXferScrFile){ char RelativeExifName[PATH_MAX+1]; // Make a relative name. RelativeName(RelativeExifName, ExifXferScrFile, FileName); if(!ReadJpegFile(RelativeExifName, READ_EXIF)) return; DiscardAllButExif(); // Don't re-read exif section again on next read. Modified = TRUE; ReadMode = READ_IMAGE; } FilesMatched += 1; FilesMatched = TRUE; // Turns off complaining that nothing matched. if (DoModify){ ReadMode |= READ_IMAGE; } if (!ReadJpegFile(FileName, ReadMode)) return; if (CheckFileSkip()){ DiscardData(); return; } if (ShowConcise){ ShowConciseImageInfo(); }else{ if (!(DoModify || DoReadAction) || ShowTags){ if (DoN3) { ShowImageInfoInN3(); } else { ShowImageInfo(); } } } if (ThumbnailName){ if (ImageInfo.ThumbnailPointer){ FILE * ThumbnailFile; char OutFileName[PATH_MAX+1]; // Make a relative name. RelativeName(OutFileName, ThumbnailName, FileName); #ifndef _WIN32 if (strcmp(ThumbnailName, "-") == 0){ // A filename of '-' indicates thumbnail goes to stdout. // This doesn't make much sense under Windows, so this feature is unix only. ThumbnailFile = stdout; }else #endif { ThumbnailFile = fopen(OutFileName,"wb"); } if (ThumbnailFile){ fwrite(ImageInfo.ThumbnailPointer, ImageInfo.ThumbnailSize ,1, ThumbnailFile); fclose(ThumbnailFile); if (ThumbnailFile != stdout){ printf("Created: '%s'\n", OutFileName); }else{ // No point in printing to stdout when that is where the thumbnail goes! } }else{ ErrFatal("Could not write thumbnail file"); } }else{ printf("Image '%s' contains no thumbnail\n",FileName); } } #ifdef MATTHIAS if (EditComment || CommentInsertfileName || AddComment || RemComment){ #else if (EditComment || CommentInsertfileName){ #endif Section_t * CommentSec; char Comment[1000]; int CommentSize; CommentSec = FindSection(M_COM); if (CommentSec == NULL){ unsigned char * DummyData; DummyData = (uchar *) malloc(3); DummyData[0] = 0; DummyData[1] = 2; DummyData[2] = 0; CommentSec = CreateSection(M_COM, DummyData, 2); } CommentSize = CommentSec->Size-2; if (CommentInsertfileName){ // Read a new comment section from file. char CommentFileName[PATH_MAX+1]; FILE * CommentFile; // Make a relative name. RelativeName(CommentFileName, CommentInsertfileName, FileName); CommentFile = fopen(CommentFileName,"r"); if (CommentFile == NULL){ printf("Could not open '%s'\n",CommentFileName); }else{ // Read it in. // Replace the section. CommentSize = fread(Comment, 1, 999, CommentFile); fclose(CommentFile); if (CommentSize < 0) CommentSize = 0; } }else{ #ifdef MATTHIAS if (ModifyDescriptComment(Comment, (char *)CommentSec->Data+2)){ Modified = TRUE; CommentSize = strlen(Comment); } if (EditComment) #else memcpy(Comment, (char *)CommentSec->Data+2, CommentSize); #endif { char EditFileName[PATH_MAX+4]; strcpy(EditFileName, FileName); strcat(EditFileName, ".txt"); CommentSize = FileEditComment(EditFileName, Comment, CommentSize); } } if (strcmp(Comment, (char *)CommentSec->Data+2)){ // Discard old comment section and put a new one in. int size; size = CommentSize+2; free(CommentSec->Data); CommentSec->Size = size; CommentSec->Data = malloc(size); CommentSec->Data[0] = (uchar)(size >> 8); CommentSec->Data[1] = (uchar)(size); memcpy((CommentSec->Data)+2, Comment, size-2); Modified = TRUE; } if (!Modified){ printf("Comment not modified\n"); } }
//-------------------------------------------------------------------------- // Apply the specified command to the jpeg file. //-------------------------------------------------------------------------- static void DoCommand(const char * FileName) { int a,e; char ExecString[400]; char TempName[200]; int TempUsed = FALSE; e = 0; // Make a temporary file in the destination directory by changing last char. strcpy(TempName, FileName); a = strlen(TempName)-1; TempName[a] = TempName[a] == 't' ? 'z' : 't'; // Build the exec string. &i and &o in the exec string get replaced by input and output files. for (a=0;;a++){ if (ApplyCommand[a] == '&'){ if (ApplyCommand[a+1] == 'i'){ // Input file. if (strstr(FileName, " ")){ e += sprintf(ExecString+e, "\"%s\"",FileName); }else{ // No need for quoting (that way I can put a relative path in front) e += sprintf(ExecString+e, "%s",FileName); } a += 1; continue; } if (ApplyCommand[a+1] == 'o'){ // Needs an output file distinct from the input file. e += sprintf(ExecString+e, "\"%s\"",TempName); a += 1; TempUsed = TRUE; unlink(TempName);// Remove any pre-existing temp file continue; } } ExecString[e++] = ApplyCommand[a]; if (ApplyCommand[a] == 0) break; } printf("Cmd:%s\n",ExecString); errno = 0; a = system(ExecString); if (a || errno){ // A command can however fail without errno getting set or system returning an error. if (errno) perror("system"); ErrFatal("Problem executing specified command"); } if (TempUsed){ // Don't delete original file until we know a new one was created by the command. struct stat dummy; if (stat(TempName, &dummy) == 0){ unlink(FileName); rename(TempName, FileName); }else{ ErrFatal("specified command did not produce expected output file"); } } }