@@ -5,7 +5,7 @@ use std::path::{Path, PathBuf};
55
66use crate :: formats:: is_raw_file;
77use crate :: image_processing:: ImageMetadata ;
8- use chrono:: { DateTime , NaiveDateTime , Utc } ;
8+ use chrono:: { DateTime , Duration , NaiveDateTime , Utc } ;
99use exif:: { Exif , In , Value } ;
1010use little_exif:: exif_tag:: ExifTag ;
1111use little_exif:: filetype:: FileExtension ;
@@ -87,6 +87,48 @@ fn clean_creation_datetime_str(s: &str) -> &str {
8787 s. trim ( ) . trim_matches ( '"' ) . trim_matches ( '\'' ) . trim ( )
8888}
8989
90+ // Parse "+HH:MM" / "-HH:MM" offset into total seconds east of UTC.
91+ fn parse_offset_secs ( s : & str ) -> Option < i64 > {
92+ let s = s. trim ( ) . trim_matches ( '"' ) ;
93+ if s. len ( ) < 6 {
94+ return None ;
95+ }
96+ let sign: i64 = if s. starts_with ( '-' ) { -1 } else { 1 } ;
97+ let rest = if s. starts_with ( '+' ) || s. starts_with ( '-' ) {
98+ & s[ 1 ..]
99+ } else {
100+ s
101+ } ;
102+ let mut parts = rest. splitn ( 2 , ':' ) ;
103+ let h = parts. next ( ) ?. parse :: < i64 > ( ) . ok ( ) ?;
104+ let m = parts. next ( ) ?. parse :: < i64 > ( ) . ok ( ) ?;
105+ Some ( sign * ( h * 3600 + m * 60 ) )
106+ }
107+
108+ // Combine a datetime string with an optional offset into EXIF inline format.
109+ // Input dt may use dashes or colons as date separator; output always uses colons.
110+ // Result: "YYYY:MM:DD HH:MM:SS+HH:MM" or "YYYY:MM:DD HH:MM:SS" if no offset.
111+ fn combine_datetime_with_offset ( dt : & str , offset : Option < & str > ) -> String {
112+ let dt = dt. trim_matches ( '"' ) . trim ( ) ;
113+ let normalized = if dt. len ( ) >= 10 {
114+ format ! ( "{}{}" , dt[ ..10 ] . replace( '-' , ":" ) , & dt[ 10 ..] )
115+ } else {
116+ dt. to_string ( )
117+ } ;
118+ let bare = if normalized. len ( ) >= 19 {
119+ & normalized[ ..19 ]
120+ } else {
121+ normalized. as_str ( )
122+ } ;
123+ match offset
124+ . map ( |s| s. trim ( ) . trim_matches ( '"' ) )
125+ . filter ( |s| !s. is_empty ( ) )
126+ {
127+ Some ( off) => format ! ( "{}{}" , bare, off) ,
128+ None => bare. to_string ( ) ,
129+ }
130+ }
131+
90132fn fmt_date_str ( s : String ) -> String {
91133 if let Some ( dt) = parse_creation_datetime ( & s) {
92134 return dt. format ( "%Y-%m-%d %H:%M:%S" ) . to_string ( ) ;
@@ -600,7 +642,29 @@ pub fn get_creation_date_from_path(path: &Path) -> DateTime<Utc> {
600642 && let Some ( dt_str) = map. get ( "DateTimeOriginal" ) . or ( map. get ( "CreateDate" ) )
601643 && let Some ( dt) = parse_creation_datetime ( dt_str)
602644 {
603- return DateTime :: from_naive_utc_and_offset ( dt, Utc ) ;
645+ let offset_secs = map
646+ . get ( "OffsetTimeOriginal" )
647+ . or_else ( || map. get ( "OffsetTime" ) )
648+ . and_then ( |s| parse_offset_secs ( s) )
649+ . or_else ( || {
650+ // Sidecar may be stale (created before OffsetTime support); read from file.
651+ if !is_raw_file ( path. to_string_lossy ( ) . as_ref ( ) ) {
652+ return None ;
653+ }
654+ let loader = rawler:: RawLoader :: new ( ) ;
655+ let raw_source = rawler:: rawsource:: RawSource :: new ( path) . ok ( ) ?;
656+ let decoder = loader. get_decoder ( & raw_source) . ok ( ) ?;
657+ let meta = decoder
658+ . raw_metadata ( & raw_source, & Default :: default ( ) )
659+ . ok ( ) ?;
660+ meta. exif
661+ . offset_time_original
662+ . as_deref ( )
663+ . or ( meta. exif . offset_time . as_deref ( ) )
664+ . and_then ( parse_offset_secs)
665+ } )
666+ . unwrap_or ( 0 ) ;
667+ return DateTime :: from_naive_utc_and_offset ( dt - Duration :: seconds ( offset_secs) , Utc ) ;
604668 }
605669
606670 if let Ok ( file) = std:: fs:: File :: open ( path) {
@@ -624,11 +688,28 @@ pub fn get_creation_date_from_path(path: &Path) -> DateTime<Utc> {
624688 && let Ok ( decoder) = loader. get_decoder ( & raw_source)
625689 && let Ok ( metadata) = decoder. raw_metadata ( & raw_source, & Default :: default ( ) )
626690 {
627- if let Some ( dt) = parse_raw_creation_date ( metadata. exif . date_time_original . as_deref ( ) ) {
628- return dt;
691+ let offset_secs = metadata
692+ . exif
693+ . offset_time_original
694+ . as_deref ( )
695+ . or ( metadata. exif . offset_time . as_deref ( ) )
696+ . and_then ( parse_offset_secs)
697+ . unwrap_or ( 0 ) ;
698+ if let Some ( dt) =
699+ parse_creation_datetime ( metadata. exif . date_time_original . as_deref ( ) . unwrap_or ( "" ) )
700+ {
701+ return DateTime :: from_naive_utc_and_offset (
702+ dt - Duration :: seconds ( offset_secs) ,
703+ Utc ,
704+ ) ;
629705 }
630- if let Some ( dt) = parse_raw_creation_date ( metadata. exif . create_date . as_deref ( ) ) {
631- return dt;
706+ if let Some ( dt) =
707+ parse_creation_datetime ( metadata. exif . create_date . as_deref ( ) . unwrap_or ( "" ) )
708+ {
709+ return DateTime :: from_naive_utc_and_offset (
710+ dt - Duration :: seconds ( offset_secs) ,
711+ Utc ,
712+ ) ;
632713 }
633714 }
634715 }
@@ -762,10 +843,27 @@ pub fn write_image_with_metadata(
762843 metadata. set_tag ( ExifTag :: ImageDescription ( clean_s ( val) ) ) ;
763844 }
764845 if let Some ( val) = map. get ( "DateTimeOriginal" ) {
765- metadata. set_tag ( ExifTag :: DateTimeOriginal ( clean_s ( val) ) ) ;
846+ let offset = map. get ( "OffsetTimeOriginal" ) . map ( String :: as_str) ;
847+ metadata. set_tag ( ExifTag :: DateTimeOriginal ( combine_datetime_with_offset (
848+ & clean_s ( val) ,
849+ offset,
850+ ) ) ) ;
766851 }
767852 if let Some ( val) = map. get ( "CreateDate" ) {
768- metadata. set_tag ( ExifTag :: CreateDate ( clean_s ( val) ) ) ;
853+ let offset = map. get ( "OffsetTime" ) . map ( String :: as_str) ;
854+ metadata. set_tag ( ExifTag :: CreateDate ( combine_datetime_with_offset (
855+ & clean_s ( val) ,
856+ offset,
857+ ) ) ) ;
858+ }
859+ if let Some ( val) = map. get ( "OffsetTime" ) {
860+ metadata. set_tag ( ExifTag :: OffsetTime ( clean_s ( val) ) ) ;
861+ }
862+ if let Some ( val) = map. get ( "OffsetTimeOriginal" ) {
863+ metadata. set_tag ( ExifTag :: OffsetTimeOriginal ( clean_s ( val) ) ) ;
864+ }
865+ if let Some ( val) = map. get ( "OffsetTimeDigitized" ) {
866+ metadata. set_tag ( ExifTag :: OffsetTimeDigitized ( clean_s ( val) ) ) ;
769867 }
770868 if let Some ( val) = map. get ( "FNumber" )
771869 && let Some ( ur) = parse_ur64 ( val)
@@ -842,10 +940,31 @@ pub fn write_image_with_metadata(
842940 metadata. set_tag ( ExifTag :: Copyright ( get_string_val ( f) ) ) ;
843941 }
844942 if let Some ( f) = exif_obj. get_field ( exif:: Tag :: DateTimeOriginal , exif:: In :: PRIMARY ) {
845- metadata. set_tag ( ExifTag :: DateTimeOriginal ( get_string_val ( f) ) ) ;
943+ let offset = exif_obj
944+ . get_field ( exif:: Tag :: OffsetTimeOriginal , exif:: In :: PRIMARY )
945+ . map ( |of| get_string_val ( of) ) ;
946+ metadata. set_tag ( ExifTag :: DateTimeOriginal ( combine_datetime_with_offset (
947+ & get_string_val ( f) ,
948+ offset. as_deref ( ) ,
949+ ) ) ) ;
846950 }
847951 if let Some ( f) = exif_obj. get_field ( exif:: Tag :: DateTime , exif:: In :: PRIMARY ) {
848- metadata. set_tag ( ExifTag :: CreateDate ( get_string_val ( f) ) ) ;
952+ let offset = exif_obj
953+ . get_field ( exif:: Tag :: OffsetTime , exif:: In :: PRIMARY )
954+ . map ( |of| get_string_val ( of) ) ;
955+ metadata. set_tag ( ExifTag :: CreateDate ( combine_datetime_with_offset (
956+ & get_string_val ( f) ,
957+ offset. as_deref ( ) ,
958+ ) ) ) ;
959+ }
960+ if let Some ( f) = exif_obj. get_field ( exif:: Tag :: OffsetTime , exif:: In :: PRIMARY ) {
961+ metadata. set_tag ( ExifTag :: OffsetTime ( get_string_val ( f) ) ) ;
962+ }
963+ if let Some ( f) = exif_obj. get_field ( exif:: Tag :: OffsetTimeOriginal , exif:: In :: PRIMARY ) {
964+ metadata. set_tag ( ExifTag :: OffsetTimeOriginal ( get_string_val ( f) ) ) ;
965+ }
966+ if let Some ( f) = exif_obj. get_field ( exif:: Tag :: OffsetTimeDigitized , exif:: In :: PRIMARY ) {
967+ metadata. set_tag ( ExifTag :: OffsetTimeDigitized ( get_string_val ( f) ) ) ;
849968 }
850969 if let Some ( f) = exif_obj. get_field ( exif:: Tag :: FNumber , exif:: In :: PRIMARY )
851970 && let exif:: Value :: Rational ( v) = & f. value
@@ -955,11 +1074,23 @@ pub fn write_image_with_metadata(
9551074 if let Some ( copyright) = exif. copyright {
9561075 metadata. set_tag ( ExifTag :: Copyright ( copyright) ) ;
9571076 }
958- if let Some ( dt) = exif. date_time_original {
959- metadata. set_tag ( ExifTag :: DateTimeOriginal ( dt) ) ;
1077+ if let Some ( ref dt) = exif. date_time_original {
1078+ let combined =
1079+ combine_datetime_with_offset ( dt, exif. offset_time_original . as_deref ( ) ) ;
1080+ metadata. set_tag ( ExifTag :: DateTimeOriginal ( combined) ) ;
1081+ }
1082+ if let Some ( ref dt) = exif. create_date {
1083+ let combined = combine_datetime_with_offset ( dt, exif. offset_time . as_deref ( ) ) ;
1084+ metadata. set_tag ( ExifTag :: CreateDate ( combined) ) ;
1085+ }
1086+ if let Some ( ot) = exif. offset_time {
1087+ metadata. set_tag ( ExifTag :: OffsetTime ( ot) ) ;
1088+ }
1089+ if let Some ( oto) = exif. offset_time_original {
1090+ metadata. set_tag ( ExifTag :: OffsetTimeOriginal ( oto) ) ;
9601091 }
961- if let Some ( dt ) = exif. create_date {
962- metadata. set_tag ( ExifTag :: CreateDate ( dt ) ) ;
1092+ if let Some ( otd ) = exif. offset_time_digitized {
1093+ metadata. set_tag ( ExifTag :: OffsetTimeDigitized ( otd ) ) ;
9631094 }
9641095 if let Some ( lens_make) = exif. lens_make {
9651096 metadata. set_tag ( ExifTag :: LensMake ( lens_make) ) ;
0 commit comments