今天是本人破壳32年纪念日。为了纪念这个伟大的日子,今日起决定开一个新坑,记录下自己平时使用数码产品时一些奇怪的需求。
问题来自于老婆大人的30岁生日。作为口头“奔三”终于实现,迈向口头“奔四”的重要时刻,我觉得平常的生日礼物都显得不够特别。思来想去,决定送一套写真,记录一下老婆大人的“少女时代”。而这个需求也就由此源起。
为了让时光都有充分的影像记录,我的所有照片都通过NAS进行管理,并通过NAS同步至我的Surface Pro和Mac Mini中。而几乎所有的主流照片管理应用都具备时间线管理功能,我认为这也是最适合浏览的照片管理方式,可以快速帮助浏览者回到当时的记忆中。当然,这样的操作方式严重依赖于照片的Exif信息,若Exif本身就是错的,时间线的组织就会相当混乱。对于某些没有Exif的照片而言,每次编辑操作都会将照片的时间置为编辑当日,对于一个有着整理强迫症的人而言,这几乎是完全不能忍受的行为。因此,在拍写真时我专门嘱咐了摄影师,要求其打开相机的Exif记录功能,并将日期和时间设置正确。摄影师答应的很爽快,我也便并未亲自检查,问题便由此产生。
拍摄完毕已经将近晚上18点,无论是夫人还是陪同拍摄的我都很疲惫,因此没有现场选片便打车回家休息了。晚些时候,摄影师将所有的底片通过网盘发给了我。应该说摄影师的拍摄水平还是过硬的,很多照片甚至不需要精修都很出色。然而在我检查Exif信息的时候头大的事情出现了——Exif信息确实是打开的,然而时间都被设置为了2020年12月23日。
好在摄影师拍摄的时候,我用手机也蹭着拍了不少。通过照片姿势动作等倒是不难还原出真实的拍摄时间。而虽然Exif日期设置错误,但照片间的拍摄间隔是不会有错的。因此,理论上只需要确定第一张照片的真实拍摄时间,再用每张照片的Exif记录时间减去第一张照片的Exif记录时间得到间隔,用真实拍摄时间加上这个间隔即能搞定每张照片的真实拍摄时间。问题被简化为了三步:
- 确定每套写真首图真实拍摄时间
- 计算该套写真照片与首图Exif记录时间间隔
- 首图真实拍摄时间+间隔=每张照片的真实拍摄时间
到这一步,唯一的问题就是照片太多了,500多张手动改的话会累死。因此一个想法自然而言的冒了出来,干脆写个程序搞定算了。
日期的计算其实很简单,现代程序语言都提供了极为丰富的日期/时间类。问题在于照片的Exif信息怎么读取和写入完全没有头绪。传统的方法应该是Google搜索“Java操作Exif”类似的关键词,得到的词条可能是某个库的文档或者一段例程。而都4202年了,为了这么一个小工具用这种方式效率太低也不Fashion,怎么也得有AI加持才行。于是乎,ChatGPT走起。输入“使用Java获取Exif拍摄日期”,便可得到如下答案:
后文中也给出了例程。到这一步其实程序便很简单了。输入两个日期,分别为首图的Exif日期和推测的真实拍摄时间,计算后通过Apache Commons Imaging修改Exif参数即可。其实这才是当前阶段AI辅助编程的重要方式,计算机科学是一门飞速发展的科学,不可能要求程序员对每个库每个API都做到了如指掌。那么在庞大繁杂的文档中快速提取有效信息,甚至于用简单描述快速创建原型Demo,便是AI可以大有可为的场景。而这种工作对于程序员的帮助是巨大的。相关代码如下:
public static void modifyExifTime(File imageFile, LocalDateTime baseTime, LocalDateTime targetTime, String operation) { try { ImageMetadata metadata = Imaging.getMetadata(imageFile); if (metadata instanceof JpegImageMetadata) { JpegImageMetadata jpegMetadata = (JpegImageMetadata) metadata; TiffImageMetadata exifMetadata = jpegMetadata.getExif(); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy:MM:dd HH:mm:ss"); Duration duration = null; if (exifMetadata != null) { String dateTaken = exifMetadata.findField(ExifTagConstants.EXIF_TAG_DATE_TIME_ORIGINAL).getValueDescription().substring(1,20); //获取原始EXIF时间 LocalDateTime originalTime = LocalDateTime.parse(dateTaken, formatter); duration = Duration.between(baseTime, originalTime); LocalDateTime resultTime = targetTime.plus(duration); if (operation.equalsIgnoreCase("modify")) { TiffOutputSet outputSet = exifMetadata != null ? exifMetadata.getOutputSet() : new TiffOutputSet(); //移除原有EXIF时间 TiffOutputDirectory exifDirectory = outputSet.getOrCreateExifDirectory(); exifDirectory.removeField(ExifTagConstants.EXIF_TAG_DATE_TIME_ORIGINAL); exifDirectory.removeField(ExifTagConstants.EXIF_TAG_DATE_TIME_DIGITIZED); //写入新EXIF时间 String resultTimeString = resultTime.format(formatter); exifDirectory.add(ExifTagConstants.EXIF_TAG_DATE_TIME_ORIGINAL, resultTimeString); File tempFile = File.createTempFile("tempImage", ".jpg"); try (FileOutputStream fos = new FileOutputStream(tempFile)) { new ExifRewriter().updateExifMetadataLossless(imageFile, fos, outputSet); } // 替换原始文件 if (imageFile.delete() && tempFile.renameTo(imageFile)) { System.out.println("拍摄日期已修改成功:" + resultTimeString); } else { System.out.println("无法替换原始文件。"); } }else { formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); String resultTimeString = resultTime.format(formatter); System.out.println("文件名:" + imageFile.getName() + "拟替换的结果时间:" + resultTimeString); } } } } catch (Exception e) { //很多图片可能读取exif出现异常为正常现象 通常无需处理 e.printStackTrace(); } }
执行的结果如下图所示。为了方便,我设置了两种操作方式。输入任意字符可输出推测的拍摄时间,可与手机拍摄的照片姿势做比对,用于微调推测的首图拍摄时间。输入“modify”则会真实写入图片的Exif信息,包括拍摄时间和数字化时间。执行完毕后在时间线中浏览图片,可以看到手机拍的图和发来的底片顺序完美融合,虽不敢说推测的拍摄时间一秒不差,但误差也在几秒之间,看起来不会有任何问题。
相关代码已开源在Github,有需要者可自取(自恋,这种破玩意谁需要):j-10cc/ExifEditor: 已知某一照片的拍摄时间,批量校正其他同批Exif拍摄时间 (github.com)
最后,我老婆真好看!(题图为AI版^_^)
代码都贴了,写真不贴一下吗
那不行。代码可以开源,老婆不行。