Batch Replace JPG Header Dates Using TextInputStream & TextOutputStream?

Google Photos does not allow the sorting of images by name in its albums (folders). Images can only be sorted by “oldest first”, “newest first” or “recently added”. In order to sort them by name you need to edit the album, manually drag them into order, and save the album.

A way to trick Google Photos to sort them by name would be to alter the EXIF dates so that the alphabetical sort order of the image file names corresponds to the sort order of their EXIF dates.

I use PhotoMill to watermark my images. It and similar products allow you to shift the EXIF capture date, but that is really a moot point as all that does is shift the original capture date, or a new capture date you establish, by the same amount for all the images in the batch.

Instead one must establish a new base date for the first image in a batch. The base date plus a increasing shift factor is then applied to each remaining image in the batch.

a 1/16/2020
b 1/17/2020
c 1/18/2020
d 1/19/2020
e 1/20/2020
f 1/21/2020
g 1/22/2020
h 1/23/2020
i 1/24/2020
j 1/25/2020
k 1/26/2020
l 1/27/2020

Each jpg I work with has four dates that need to be changed. The X’s represent the placement of other image file data.

XXXXXXXXXX2020:01:16 13:07:04XXXXXXXXXX2020:01:16 12:13:02XXXXXXXXXX2020-01-16T13:07:04XXXXXXXXXX2020-01-16T12:13:02XXXXXXXXXX

This is my first draft of a simple solution.

This code:

  1. Allows a user to select a folder.

  2. Creates a new folder in the selected folder’s parent folder to hold new date altered versions of the images so the originals are not overwritten.

  3. Establishes a new base date as today’s date.

  4. Loops through the images in the folder and replaces the four dates. The first image is given the base date while each remaining images have 1 additional day added.

  5. For good measure the file creation and modification date are also changed.

This code causes a spinning beach ball in the Action event of Button so I’ve moved it into a thread with a timer updating a progress bar .

I’ve checked the EXIF information with several external tools and it seems to work; however, perhaps there is a more simple solution.

Although the images now have new dates Google Photos still does not sort all of them in the correct order. This is likely due to Google Photos algorithms being flawed. If one sorts images using “oldest first” and then switches to “newest first” the images should be exactly in opposite order, but they are not - which seems to indicate their sorting process is corrupted.

  Dim F, FC  As FolderItem
  Dim Count as Integer
  
  F = SelectFolder
  
  If F <> Nil then
    
    Window1.Title = F.Name
    Count = F.Count
    
    If Count > 0 then
      
      FC = F.Parent.Child("EXIF " + F.Name)
      
      If FC.Exists then
        FC.Delete
      Else
        FC.CreateAsFolder
      End If
      
      Window1.Title = F.Name
      Count = F.Count
      
      Dim F2, I as FolderItem
      Dim DoIt, DoIt2  as Integer
      Dim DS, S3, NewDate, S, S2(-1) as String
      Dim StreamIn as TextInputStream
      Dim StreamOut as TextOutputStream
      Dim CDate as New Date
      
      For DoIt = 1 to Count
        
        I = F.Item(DoIt)
        
        If I <> Nil and I.Visible = True and Right(I.Name, 4) = ".jpg" then
          
          StreamIn = TextInputStream.Open(I)
          S = StreamIn.ReadAll
          StreamIn.Close
          
          S2 = Split(S, "2020")
          S3 = S
          
          CDate.TotalSeconds = CDate.TotalSeconds  + 86400
          
          For DoIt2 = 1 to UBound(S2)
            
            DS = "2020" + Left(S2(DoIt2), 6)
            
            If Instr(DS, "-") > 0 then
              NewDate = Str(CDate.Year) + "-" + Str(Format(CDate.Month, "0#")) + "-" + Str(Format(CDate.Day, "0#"))
            Else
              NewDate = Str(CDate.Year) + ":" + Str(Format(CDate.Month, "0#")) + ":" + Str(Format(CDate.Day, "0#"))
            End If
            
            S3 = ReplaceAll(S3, DS, NewDate)
            
          Next
          
          F2 = FC.Child(I.Name)
          StreamOut = TextOutputStream.Create(F2)
          StreamOut.Write S3
          StreamOut.Close
          
          F2.CreationDate = CDate
          F2.ModificationDate = CDate
          
        End If
        
      Next
      
    Else
      
      MsgBox "Folder is Empty!"
      
    End If
    
  Else
    
    //User Cancelled
    
  End If

I’d use binary streams since text input and output streams assume you are writing “text” which JPG’s most definitely are not
Using a text input / output stream may corrupt your image

  1. Don’t do this.

JPEGs use a public protocol for managing meta data, which is based on TIFF, which is based on IFF. If you follow this protocol using a binary stream (not text stream) you’ll be able to modify the date safely for any JPEG.

Also be aware that the meta data contains many sub directorys where there can be many instances of dates. TIFF, IPTC, Adobe XML and the newer extended EXIF information. I realize that this is pie in the sky, but it would really help to know which dates Google uses to sort the images.

On the Mac, there is API for manipulating the meta data of images, which doesn’t involve a JPEG re-encode. I have a feature request somewhere for Xojo to wrap this and bring it into the Xojo Framework.

Text streams will corrupt binary data.
For dates in EXIF metadata it may work to just find them by the bytes before the entry.

It appears that Google Photos uses DateTimeOriginal. If that date is absent it uses the upload time. If a photo uses a future date Google Photos uses the completed upload time instead.

I had previously been creating a new base date using the current date and adding a day to each photo in the batch. Some photos in the batch were thus post dated which Google Photos would ignore. This explains why multiple metadata checking tools were reporting the metadata as correct but why the images were not being sorted correctly.

I now subject a year from the base date before using it, which should not present any problems unless the batch is larger than a year. I’ve also switched over to binary as suggested.

I edit images using Photoshop and then watermark then using PhotoMill. PhotoMill does allow one to set a new capture date and to shift the capture date buts its the same shift for all images in a batch so that can’t be used. I instead use PhotoMill to reset the capture date to the same date for all photos watermarked so my Xojo has a standard date to replace.

The new capture date is stored six times in the metadata, in plain ASCII format, using two formats of three occurences each.

2020:01:16 00:00:00
2020-01-16T00:00:00

On a batch of 27 images that are 1.7 to 1.9MB each this code takes less than 2 seconds to complete. The images are correctly sorted by Google Photos.

I’ve also recently found that the old iPhotos has a Batch Change feature. This much slower as you first need to change the date which takes 5 seconds and then you need to export new copies of the images (11 seconds).

 Dim F, FC  As FolderItem
  Dim Count as Integer
  
  F = SelectFolder
  
  If F <> Nil then
    
    Window1.Title = F.Name
    Count = F.Count
    
    If Count > 0 then
      
      FC = F.Parent.Child("EXIF " + F.Name)
      
      If FC.Exists then
        FC.Delete
      Else
        FC.CreateAsFolder
      End If
      
      Window1.Title = F.Name
      Count = F.Count
      
      Dim F2, I as FolderItem
      Dim DoIt, DoIt2  as Integer
      Dim DS, S3, NewDate1, NewDate2, S, S2(-1) as String
      Dim StreamIn as BinaryStream
      Dim StreamOut as BinaryStream
      Dim CDate as New Date
      
      CDate.TotalSeconds = CDate.TotalSeconds - 31536000
      
      For DoIt = 1 to Count
        
        I = F.Item(DoIt)
        
        If I <> Nil and I.Visible = True and Right(I.Name, 4) = ".jpg" then
          
          StreamIn = BinaryStream.Open(I, False)
          S = StreamIn.Read(StreamIn.Length)
          
          StreamIn.Close
          
          S3 = S
          
          CDate.TotalSeconds = CDate.TotalSeconds  + 86400
          
          NewDate1 =  Str(CDate.Year) + ":" + Str(Format(CDate.Month, "0#")) + ":" + Str(Format(CDate.Day, "0#"))
          
          NewDate2 = Str(CDate.Year) + "-" + Str(Format(CDate.Month, "0#")) + "-" + Str(Format(CDate.Day, "0#"))
          
          S3 = ReplaceAllB(S3, "2020:01:16", NewDate1)
          S3 = ReplaceAllB(S3, "2020-01-16", NewDate2)
          
          F2 = FC.Child(I.Name)
          StreamOut = BinaryStream.Create(F2, True)
          
          StreamOut.Write S3
          StreamOut.Close
          
          F2.CreationDate = CDate
          F2.ModificationDate = CDate
          
        End If
        
      Next
      
    Else
      
      MsgBox "Folder is Empty!"
      
    End If
    
  Else
    
    //User Cancelled
    
  End If