Photo Date-Time Metadata and Annotations

This topic has been viewed 6719 times and has 3 replies
Thread status: Active
Questions about extending features and developing plug-ins [img]http://www.sweethome3d.com/images/flags/en_small.gif[/img]
Post Reply New Topic
User avatar
digitaltrails
Posts: 95
Joined: Sun Dec 28, 2014 12:20 am
Country: Wellington, New Zealand, New Zealand
Contact:

Photo Date-Time Metadata and Annotations

Post by digitaltrails »

I originally emailed info@eteks, but having got no response, I thought this forum might be the more correct place for development suggestions.

I've been experimenting with sun angles at different times of the year.  I found it useful to write the as-at date-time into the PNG to keep track of which date-times each images illustrates. For example:

Code: Select all

   % exiftool -a ~/3D/test2.png
    ExifTool Version Number         : 9.34
    File Name                       : test2.png
    Directory                       : /home/michael/3D
    File Size                       : 30 kB
    File Modification Date/Time     : 2015:02:13 22:51:30+13:00
    ...
    Create Date Time                : 2015.02.13 22:51:16+13:00
    Date Time Original              : 2015.02.13 12:00:00+13:00
    Image Size                      : 400x223
As well as this I added code to optionally annotate the actual PNG graphic - note the Add Date check-box and displayed dates in the following image:

Image

Patch is as follows (only tested with English):

Code: Select all

diff -r -u ../SweetHome3D-4.6-src-original/src/com/eteks/sweethome3d/swing/package.properties src/com/eteks/sweethome3d/swing/package.properties
--- ../SweetHome3D-4.6-src-original/src/com/eteks/sweethome3d/swing/package.properties	2015-02-02 21:39:30.000000000 +1300
+++ src/com/eteks/sweethome3d/swing/package.properties	2015-02-13 21:44:57.009095228 +1300
@@ -1539,6 +1539,8 @@
 PhotoSizeAndQualityPanel.heightLabel.mnemonic=H
 PhotoSizeAndQualityPanel.applyProportionsCheckBox.text=Apply proportions:
 PhotoSizeAndQualityPanel.applyProportionsCheckBox.mnemonic=A
+PhotoSizeAndQualityPanel.addDateCheckBox.text=Add Date
+PhotoSizeAndQualityPanel.addDateCheckBox.mnemonic=D
 PhotoSizeAndQualityPanel.aspectRatioComboBox.view3DRatio.text=3D view
 PhotoSizeAndQualityPanel.aspectRatioComboBox.squareRatio.text=Square
 PhotoSizeAndQualityPanel.qualityLabel.text=Quality:
diff -r -u ../SweetHome3D-4.6-src-original/src/com/eteks/sweethome3d/swing/PhotoPanel.java src/com/eteks/sweethome3d/swing/PhotoPanel.java
--- ../SweetHome3D-4.6-src-original/src/com/eteks/sweethome3d/swing/PhotoPanel.java	2015-02-02 21:39:30.000000000 +1300
+++ src/com/eteks/sweethome3d/swing/PhotoPanel.java	2015-02-13 22:50:38.674348690 +1300
@@ -20,10 +20,12 @@
 package com.eteks.sweethome3d.swing;
 
 import java.awt.CardLayout;
+import java.awt.Color;
 import java.awt.Component;
 import java.awt.ComponentOrientation;
 import java.awt.Dimension;
 import java.awt.EventQueue;
+import java.awt.Graphics;
 import java.awt.Graphics2D;
 import java.awt.GridBagConstraints;
 import java.awt.GridBagLayout;
@@ -62,7 +64,13 @@
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 
+import javax.imageio.IIOImage;
 import javax.imageio.ImageIO;
+import javax.imageio.ImageTypeSpecifier;
+import javax.imageio.ImageWriteParam;
+import javax.imageio.ImageWriter;
+import javax.imageio.metadata.IIOMetadata;
+import javax.imageio.metadata.IIOMetadataNode;
 import javax.swing.ActionMap;
 import javax.swing.DefaultListCellRenderer;
 import javax.swing.Icon;
@@ -139,6 +147,7 @@
   private JButton                  createButton;
   private JButton                  saveButton;
   private JButton                  closeButton;
+  private long photoDateTimeOriginal;
 
   private static PhotoPanel        currentPhotoPanel; // Support only one photo panel opened at a time
 
@@ -761,6 +770,8 @@
         int bestImageHeight;
         // Check correct ratio if lens is fisheye or spherical
         Camera camera = home.getCamera();
+        // Seem to be in local time - adjust back to UTC
+        photoDateTimeOriginal = camera.getTime() - TimeZone.getDefault().getOffset(camera.getTime());
         if (camera.getLens() == Camera.Lens.FISHEYE) {
           bestImageHeight = imageWidth;
         } else if (camera.getLens() == Camera.Lens.SPHERICAL) {
@@ -781,6 +792,7 @@
         }
       } else {
         // Compute 3D view offscreen image
+        photoDateTimeOriginal = System.currentTimeMillis();
         HomeComponent3D homeComponent3D = new HomeComponent3D(
             home, this.preferences, this.object3dFactory, quality == 1, null);
         image = homeComponent3D.getOffScreenImage(imageWidth, imageHeight);
@@ -802,6 +814,9 @@
             getActionMap().get(ActionType.SAVE_PHOTO).setEnabled(photoImage != null);
             if (photoImage != null) {
               getRootPane().setDefaultButton(saveButton);
+              if (controller.isDateShown()) {
+                annotatePhoto(photoImage, photoCreationStartTime, photoDateTimeOriginal);
+              }
             }
             createButton.setAction(getActionMap().get(ActionType.START_PHOTO_CREATION));
             photoComponent.setImage(photoImage);
@@ -854,14 +869,16 @@
 
   /**
    * Saves the created image.
+   * @param showDate 
    */
   private void savePhoto() {
     String pngFile = this.controller.getContentManager().showSaveDialog(this,
         this.preferences.getLocalizedString(PhotoPanel.class, "savePhotoDialog.title"), 
         ContentManager.ContentType.PNG, this.home.getName());
     try {
-      if (pngFile != null) {
-        ImageIO.write(this.photoComponent.getImage(), "PNG", new File(pngFile));
+      if (pngFile != null) {    
+        writePhoto(this.photoComponent.getImage(), "PNG", new File(pngFile), this.photoCreationStartTime, this.photoDateTimeOriginal);
+        //ImageIO.write(this.photoComponent.getImage(), "PNG", new File(pngFile));
       }
     } catch (IOException ex) {
       String messageFormat = this.preferences.getLocalizedString(PhotoPanel.class, "savePhotoError.message");
@@ -870,6 +887,51 @@
     }
   }
 
+  private void writePhoto(BufferedImage image, String string, File file, long createDateTime, long dateTimeOriginal) throws IOException {
+    SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy.MM.dd HH:mm:ssXXX"); 
+
+    String createTime = dateFormatter.format(createDateTime);
+    IIOMetadataNode textEntry1 = new IIOMetadataNode("tEXtEntry");
+    textEntry1.setAttribute("keyword", "CreateDateTime");
+    textEntry1.setAttribute("value", createTime);
+    IIOMetadataNode textNode1 = new IIOMetadataNode("tEXt");
+    textNode1.appendChild(textEntry1);
+    
+    String photoTime = dateFormatter.format(dateTimeOriginal);
+    IIOMetadataNode textEntry2 = new IIOMetadataNode("tEXtEntry");
+    textEntry2.setAttribute("keyword", "DateTimeOriginal");
+    textEntry2.setAttribute("value", photoTime);
+    IIOMetadataNode textNode2 = new IIOMetadataNode("tEXt");
+    textNode2.appendChild(textEntry2);
+    
+    IIOMetadataNode root = new IIOMetadataNode("javax_imageio_png_1.0");
+    root.appendChild(textNode1);
+    root.appendChild(textNode2);
+
+    ImageWriter writer = ImageIO.getImageWritersByFormatName("png").next();
+    ImageWriteParam writeParam = writer.getDefaultWriteParam();
+    ImageTypeSpecifier imageTypeSpecifier = ImageTypeSpecifier.createFromBufferedImageType(image.getType());
+    IIOMetadata imageMetadata = writer.getDefaultImageMetadata(imageTypeSpecifier, writeParam);
+    imageMetadata.mergeTree("javax_imageio_png_1.0", root);
+
+    IIOImage imageWithMetadata = new IIOImage(image, null, imageMetadata);
+    Object stream = ImageIO.createImageOutputStream(file);
+    writer.setOutput(stream);
+    
+    writer.write(imageMetadata, imageWithMetadata, writeParam);   
+  }
+
+  private void annotatePhoto(BufferedImage image, long createDateTime, long dateTimeOriginal) {
+    SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy.MM.dd HH:mm:ssXXX"); 
+    String photoTime = dateFormatter.format(dateTimeOriginal);
+    String createTime = dateFormatter.format(createDateTime);
+    Graphics graphics = image.getGraphics();
+    graphics.setFont(graphics.getFont().deriveFont(10f));
+    graphics.setColor(Color.RED);
+    graphics.drawString(photoTime + " (" + createTime + ")", 20, 20);
+    graphics.dispose();
+  }
+  
   /**
    * Manages closing of this pane.
    */
Only in src/com/eteks/sweethome3d/swing: PhotoPanel.java.dateTimeOriginal
Only in src/com/eteks/sweethome3d/swing: PhotoPanel.java.keep
diff -r -u ../SweetHome3D-4.6-src-original/src/com/eteks/sweethome3d/swing/PhotoSizeAndQualityPanel.java src/com/eteks/sweethome3d/swing/PhotoSizeAndQualityPanel.java
--- ../SweetHome3D-4.6-src-original/src/com/eteks/sweethome3d/swing/PhotoSizeAndQualityPanel.java	2015-02-02 21:39:30.000000000 +1300
+++ src/com/eteks/sweethome3d/swing/PhotoSizeAndQualityPanel.java	2015-02-13 21:52:42.458590124 +1300
@@ -72,6 +72,7 @@
   private JLabel                        heightLabel;
   private JSpinner                      heightSpinner;
   private JCheckBox                     applyProportionsCheckBox;
+  private JCheckBox                     addDateCheckBox;
   private JComboBox                     aspectRatioComboBox;
   private JLabel                        qualityLabel;
   private JSlider                       qualitySlider;
@@ -141,6 +142,14 @@
               : AspectRatio.FREE_RATIO);
         }
       });
+    
+    this.addDateCheckBox = new JCheckBox();
+    this.addDateCheckBox.setSelected(controller.isDateShown());
+    this.addDateCheckBox.addItemListener(new ItemListener() {
+        public void itemStateChanged(ItemEvent ev) {
+          controller.setDateShown(addDateCheckBox.isSelected());
+        }
+      });
     this.aspectRatioComboBox = new JComboBox(new Object [] {
         AspectRatio.VIEW_3D_RATIO,
         AspectRatio.SQUARE_RATIO,
@@ -196,6 +205,7 @@
             applyProportionsCheckBox.setSelected(notFreeAspectRatio);
             aspectRatioComboBox.setEnabled(notFreeAspectRatio);
             aspectRatioComboBox.setSelectedItem(controller.getAspectRatio());
+            addDateCheckBox.setSelected(controller.isDateShown());
           }
         });
 
@@ -309,6 +319,8 @@
         PhotoSizeAndQualityPanel.class, "heightLabel.text"));
     this.applyProportionsCheckBox.setText(SwingTools.getLocalizedLabelText(preferences, 
         PhotoSizeAndQualityPanel.class, "applyProportionsCheckBox.text"));
+    this.addDateCheckBox.setText(SwingTools.getLocalizedLabelText(preferences, 
+        PhotoSizeAndQualityPanel.class, "addDateCheckBox.text"));
     this.qualityLabel.setText(SwingTools.getLocalizedLabelText(preferences, 
         PhotoSizeAndQualityPanel.class, "qualityLabel.text"));
     this.fastQualityLabel.setText(SwingTools.getLocalizedLabelText(preferences,
@@ -333,6 +345,8 @@
       this.heightLabel.setLabelFor(this.heightSpinner);
       this.applyProportionsCheckBox.setMnemonic(KeyStroke.getKeyStroke(preferences.getLocalizedString(
           PhotoSizeAndQualityPanel.class, "applyProportionsCheckBox.mnemonic")).getKeyCode());
+      this.addDateCheckBox.setMnemonic(KeyStroke.getKeyStroke(preferences.getLocalizedString(
+          PhotoSizeAndQualityPanel.class, "addDateCheckBox.mnemonic")).getKeyCode());
       this.qualityLabel.setDisplayedMnemonic(KeyStroke.getKeyStroke(preferences.getLocalizedString(
           PhotoSizeAndQualityPanel.class, "qualityLabel.mnemonic")).getKeyCode());
       this.qualityLabel.setLabelFor(this.qualitySlider);
@@ -394,6 +408,7 @@
     JPanel proportionsPanel = new JPanel();
     proportionsPanel.add(this.applyProportionsCheckBox);
     proportionsPanel.add(this.aspectRatioComboBox);
+    proportionsPanel.add(this.addDateCheckBox);
     add(proportionsPanel, new GridBagConstraints(
         0, 1, 4, 1, 0, 0, GridBagConstraints.CENTER, 
         GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0));
@@ -423,6 +438,7 @@
     this.widthSpinner.setEnabled(enabled);
     this.heightSpinner.setEnabled(enabled);
     this.applyProportionsCheckBox.setEnabled(enabled);
+    this.addDateCheckBox.setEnabled(enabled);
     this.aspectRatioComboBox.setEnabled(enabled);
     this.qualitySlider.setEnabled(enabled);
   }
diff -r -u ../SweetHome3D-4.6-src-original/src/com/eteks/sweethome3d/viewcontroller/AbstractPhotoController.java src/com/eteks/sweethome3d/viewcontroller/AbstractPhotoController.java
--- ../SweetHome3D-4.6-src-original/src/com/eteks/sweethome3d/viewcontroller/AbstractPhotoController.java	2015-02-02 21:39:30.000000000 +1300
+++ src/com/eteks/sweethome3d/viewcontroller/AbstractPhotoController.java	2015-02-13 21:52:42.439590123 +1300
@@ -51,6 +51,7 @@
   private int                         quality;
   private float                       view3DAspectRatio;
   private int                         ceilingLightColor;
+  private boolean                     dateShown;
 
   public AbstractPhotoController(Home home,
                                  UserPreferences preferences,
@@ -309,4 +310,14 @@
                                 Object propertyValue) {
     this.home.setVisualProperty(propertyName, propertyValue);
   }
+
+  public boolean isDateShown() {
+    return this.dateShown;
+  }
+
+  public void setDateShown(boolean dateShown) {
+    this.dateShown = dateShown;
+  }
+  
+  
 }
okh
Posts: 1545
Joined: Sun May 12, 2013 9:43 pm

Re: Photo Date-Time Metadata and Annotations

Post by okh »

Nice idea! How about the geo fields (LAT/LON) which also have a great impact on how the sun renders... ?
User avatar
digitaltrails
Posts: 95
Joined: Sun Dec 28, 2014 12:20 am
Country: Wellington, New Zealand, New Zealand
Contact:

Re: Photo Date-Time Metadata and Annotations

Post by digitaltrails »

Nice idea! How about the geo fields (LAT/LON) which also have a great impact on how the sun renders... ?
That would be possible.

Extra info such as the render camera settings would definitely be useful.

I did consider embedding the geo data, but I wasn't sure I wanted GPS coordinates to my house buried inside the images, especially if it was a overhead plan view (I'm sure you're all nice people, but...). If this were to be done, I think it would have to be controlled by a setting that would default to off.

If the eTeks decide to fold this code in, I don't mind putting it some effort to include a few more attributes.

Initially I wasn't sure whether displaying text on the image was totally necessary. But metadata embedded in PNG's is only supported by a few imaging tools. It turns out there is no well established metadata standard for PNG's (that came as a bit of a shock). That pushed me to experimenting with an option for annotating the actual image.
okh
Posts: 1545
Joined: Sun May 12, 2013 9:43 pm

Re: Photo Date-Time Metadata and Annotations

Post by okh »

It turns out there is no well established metadata standard for PNG's...
Yes, isn't that strange? Has puzzled me too (even if ExifTool, I think, has set some sort of standard?). But then, (standardised) meta-data in general, is not very often a priority. About the GEO data, I certainly see the pricacy issues (could be a good idea to remove compass data when posting .sh3d files). ok
Post Reply New Topic

Who is online

Users browsing this forum: No registered users and 1 guest