Skip to main content

exiftool_rs/
composite.rs

1//! Composite (derived/calculated) tags.
2//!
3//! These tags are computed from other tags, not stored in the file.
4//! Mirrors ExifTool's Composite tags.
5
6use crate::tag::{Tag, TagGroup, TagId};
7use crate::value::Value;
8
9/// Generate composite tags from existing tags.
10pub fn compute_composite_tags(tags: &[Tag]) -> Vec<Tag> {
11    let mut composite = Vec::new();
12
13    // Canon-specific composites first — they produce ISO, WB_RGGBLevels, FlashType, etc.
14    // that are needed by later composites (LightValue needs ISO, RedBalance needs WB_RGGBLevels)
15    if let Some(canon_tags) = compute_canon_composites(tags) {
16        composite.extend(canon_tags);
17    }
18
19    if let Some(rot) = compute_quicktime_rotation(tags) {
20        composite.push(rot);
21    }
22
23    if let Some(flash) = compute_xmp_flash(tags) {
24        composite.push(flash);
25    }
26
27    if let Some(es) = compute_olympus_extender_status(tags) {
28        composite.push(es);
29    }
30
31    if let Some(fd) = compute_sony_focus_distance2(tags) {
32        composite.push(fd);
33    }
34
35    // GPSPosition: combine GPSLatitude/Ref + GPSLongitude/Ref
36    if let Some(pos) = compute_gps_position(tags) {
37        composite.push(pos);
38    }
39
40    // GPSAltitude: combine GPSAltitude + GPSAltitudeRef
41    if let Some(alt) = compute_gps_altitude(tags) {
42        composite.push(alt);
43    }
44
45    // ShutterSpeed: from ExposureTime
46    if let Some(ss) = compute_shutter_speed(tags) {
47        composite.push(ss);
48    }
49
50    // Aperture: from FNumber
51    if let Some(ap) = compute_aperture(tags) {
52        composite.push(ap);
53    }
54
55    // ShutterSpeed from ShutterSpeedValue (APEX) if no ExposureTime
56    if find_tag(tags, "ShutterSpeed").is_none() && find_tag(tags, "ExposureTime").is_none() {
57        if let Some(ssv) = find_tag_f64(tags, "ShutterSpeedValue") {
58            // Perl Composite ShutterSpeed: PrintExposureTime(2^-ApertureValue), no " s".
59            let print = crate::tags::canon_sub::print_exposure_time(2.0_f64.powf(-ssv));
60            composite.push(mk_composite(
61                "ShutterSpeed",
62                "Shutter Speed",
63                Value::String(print),
64            ));
65        }
66    }
67
68    // PanasonicRaw: ImageWidth/ImageHeight composites from sensor borders
69    // Perl PanasonicRaw::Composite: ImageWidth = SensorRightBorder - SensorLeftBorder
70    //                               ImageHeight = SensorBottomBorder - SensorTopBorder
71    // Only emit when these sensor border tags are present (RW2 files)
72    if find_tag(tags, "SensorRightBorder").is_some() && find_tag(tags, "SensorLeftBorder").is_some()
73    {
74        if let (Some(right), Some(left)) = (
75            find_tag(tags, "SensorRightBorder").and_then(|t| t.raw_value.as_u64()),
76            find_tag(tags, "SensorLeftBorder").and_then(|t| t.raw_value.as_u64()),
77        ) {
78            composite.push(mk_composite(
79                "ImageWidth",
80                "Image Width",
81                Value::String(format!("{}", right.saturating_sub(left))),
82            ));
83        }
84    }
85    if find_tag(tags, "SensorBottomBorder").is_some() && find_tag(tags, "SensorTopBorder").is_some()
86    {
87        if let (Some(bottom), Some(top)) = (
88            find_tag(tags, "SensorBottomBorder").and_then(|t| t.raw_value.as_u64()),
89            find_tag(tags, "SensorTopBorder").and_then(|t| t.raw_value.as_u64()),
90        ) {
91            composite.push(mk_composite(
92                "ImageHeight",
93                "Image Height",
94                Value::String(format!("{}", bottom.saturating_sub(top))),
95            ));
96        }
97    }
98
99    // ImageSize: Width x Height
100    {
101        let mut all: Vec<Tag> = tags.to_vec();
102        all.extend(composite.iter().cloned());
103        if let Some(mut sz) = compute_image_size(&all) {
104            // The computed ImageSize is authoritative over an extracted same-named tag
105            // (e.g. an APP12 PictureInfo ImageSize), matching ExifTool's Composite priority.
106            sz.priority = 1;
107            composite.push(sz);
108        }
109    }
110
111    // Megapixels (needs ImageSize composite)
112    {
113        let mut all: Vec<Tag> = tags.to_vec();
114        all.extend(composite.iter().cloned());
115        if let Some(mut mp) = compute_megapixels(&all) {
116            mp.priority = 1;
117            composite.push(mp);
118        }
119    }
120
121    // LightValue
122    // LightValue (needs ShutterSpeed composite)
123    {
124        let mut all: Vec<Tag> = tags.to_vec();
125        all.extend(composite.iter().cloned());
126        if let Some(lv) = compute_light_value(&all) {
127            composite.push(lv);
128        }
129    }
130
131    // SubSecDateTimeOriginal
132    if let Some(t) = make_subsec_date(
133        tags,
134        "DateTimeOriginal",
135        "SubSecTimeOriginal",
136        "OffsetTimeOriginal",
137        "SubSecDateTimeOriginal",
138    ) {
139        composite.push(t);
140    }
141    // SubSecCreateDate
142    if let Some(t) = make_subsec_date(
143        tags,
144        "CreateDate",
145        "SubSecTimeDigitized",
146        "OffsetTimeDigitized",
147        "SubSecCreateDate",
148    ) {
149        composite.push(t);
150    }
151    // SubSecModifyDate
152    if let Some(t) = make_subsec_date(
153        tags,
154        "ModifyDate",
155        "SubSecTime",
156        "OffsetTime",
157        "SubSecModifyDate",
158    ) {
159        composite.push(t);
160    }
161
162    // Geolocation is opt-in (ExifTool's `Geolocation` API option): emitted by the
163    // caller, gated on `Options::geolocation`. See `compute_geolocation`.
164
165    // ScaleFactor35efl + FocalLength35efl + Lens35efl
166    if let Some(sf_tags) = compute_35efl(tags) {
167        composite.extend(sf_tags);
168    } else if find_tag(tags, "FocalLength").is_some() {
169        // Fallback: FocalLength35efl = FocalLength when no scale factor available
170        // (Perl does: ValueConv => ($val[0] || 0) * ($val[1] || 1))
171        let fl = find_tag_f64(tags, "FocalLength").unwrap_or(0.0);
172        composite.push(mk_composite(
173            "FocalLength35efl",
174            "Focal Length (35mm equiv)",
175            Value::String(format!("{:.1} mm", fl)),
176        ));
177    }
178
179    // RedBalance + BlueBalance — search in raw tags + Canon composites (which may include WB_RGGBLevels)
180    {
181        let mut all_for_wb: Vec<Tag> = tags.to_vec();
182        all_for_wb.extend(composite.iter().cloned());
183        if let Some(wb_tags) = compute_wb_balance(&all_for_wb) {
184            composite.extend(wb_tags);
185        }
186    }
187
188    // DOF (Depth of Field) — needs CircleOfConfusion from 35efl composites
189    {
190        let mut all_tags: Vec<&Tag> = tags.iter().collect();
191        let comp_refs: Vec<&Tag> = composite.iter().collect();
192        all_tags.extend(comp_refs);
193        let all_slice: Vec<Tag> = all_tags.into_iter().cloned().collect();
194        if let Some(dof_tags) = compute_dof(&all_slice) {
195            composite.extend(dof_tags);
196        }
197        // HyperfocalDistance (needs CircleOfConfusion from composites)
198        if let Some(hd) = compute_hyperfocal(&all_slice) {
199            composite.push(hd);
200        }
201    }
202
203    // IPTC DateTimeCreated (from IPTC:DateCreated + IPTC:TimeCreated)
204    // Only combine when the source DateCreated tag is from IPTC (not RIFF, XMP, etc.)
205    if find_tag(tags, "DateTimeCreated").is_none() {
206        if let (Some(date_tag), Some(time)) = (
207            find_tag_in_group(tags, "DateCreated", "IPTC"),
208            find_tag_value(tags, "TimeCreated"),
209        ) {
210            let date = date_tag.print_value.clone();
211            if !date.is_empty() && !time.is_empty() {
212                composite.push(mk_composite(
213                    "DateTimeCreated",
214                    "Date/Time Created",
215                    Value::String(format!("{} {}", date, time)),
216                ));
217            }
218        }
219    }
220
221    // DateTimeOriginal fallback (when no EXIF DateTimeOriginal)
222    // Works from any DateCreated+TimeCreated pair (IPTC, RIFF, etc.)
223    if find_tag(tags, "DateTimeOriginal").is_none() {
224        if let (Some(date), Some(time)) = (
225            find_tag_value(tags, "DateCreated"),
226            find_tag_value(tags, "TimeCreated"),
227        ) {
228            if !date.is_empty() && !time.is_empty() {
229                composite.push(mk_composite(
230                    "DateTimeOriginal",
231                    "Date/Time Original",
232                    Value::String(format!("{} {}", date, time)),
233                ));
234            }
235        }
236    }
237    // DateTimeOriginal from ID3:Year (when no other DateTimeOriginal)
238    // Perl: ID3::Composite, only fires for ID3 group tags
239    if find_tag(tags, "DateTimeOriginal").is_none()
240        && composite.iter().all(|t| t.name != "DateTimeOriginal")
241    {
242        if let Some(year_tag) = tags
243            .iter()
244            .find(|t| t.name == "Year" && t.group.family0 == "ID3")
245        {
246            let year = year_tag.print_value.clone();
247            if !year.is_empty() {
248                composite.push(mk_composite(
249                    "DateTimeOriginal",
250                    "Date/Time Original",
251                    Value::String(year),
252                ));
253            }
254        }
255    }
256
257    // GPSDateTime composite (Perl: Require GPSDateStamp + GPSTimeStamp)
258    // Both tags must EXIST. Date can be empty (result: " 00:00:00Z")
259    if find_tag(tags, "GPSDateStamp").is_some() && find_tag(tags, "GPSTimeStamp").is_some() {
260        let date = find_tag_value(tags, "GPSDateStamp").unwrap_or_default();
261        let time = find_tag_value(tags, "GPSTimeStamp").unwrap_or_default();
262        if !time.is_empty() {
263            composite.push(mk_composite(
264                "GPSDateTime",
265                "GPS Date/Time",
266                Value::String(format!("{} {}Z", date, time)),
267            ));
268        }
269    }
270
271    // DigitalCreationDateTime (IPTC composite)
272    if let (Some(date), Some(time)) = (
273        find_tag_value(tags, "DigitalCreationDate"),
274        find_tag_value(tags, "DigitalCreationTime"),
275    ) {
276        if !date.is_empty() && !time.is_empty() {
277            composite.push(mk_composite(
278                "DigitalCreationDateTime",
279                "Digital Creation Date/Time",
280                Value::String(format!("{} {}", date, time)),
281            ));
282        }
283    }
284
285    // The Composite LensID's print conversion prefers LensType2 over LensType
286    // when it is there and valid -- "0x8000 or greater; 0 for several
287    // older/3rd-party E-mount lenses" (Exif.pm) -- which is how a Sony E-mount
288    // body names the lens its LensType of 65535 cannot. It is the same tag, so
289    // this replaces whatever the LensType route worked out.
290    if let Some(t2) = tags.iter().find(|t| t.name == "LensType2") {
291        let raw = t2.raw_value.as_u64().unwrap_or(0);
292        let named = t2.print_value != t2.raw_value.to_display_string();
293        if named && (raw & 0x8000 != 0 || raw == 0) {
294            composite.retain(|t| t.name != "LensID");
295            composite.push(mk_composite(
296                "LensID",
297                "Lens ID",
298                Value::String(t2.print_value.clone()),
299            ));
300        }
301    }
302
303    // LensID fallback: use LensModel, Lens, or LensType if no LensID computed by 35efl
304    // Only create when the value looks like a real camera lens (contains "mm" or "f/")
305    if !composite.iter().any(|t| t.name == "LensID") {
306        // Skip the "Lens" fallback when a Nikon LensData hex lookup is possible below
307        // (LensIDNumber present) — that yields the full lens name, not just "18-70mm".
308        let nikon_lensid_possible = find_tag(tags, "LensIDNumber").is_some();
309        let lens_val = find_tag_value(tags, "LensModel")
310            .filter(|v| !v.is_empty() && (v.contains("mm") || v.to_lowercase().contains("f/")))
311            .or_else(|| {
312                if nikon_lensid_possible {
313                    return None;
314                }
315                find_tag_value(tags, "Lens")
316                    .filter(|v| !v.is_empty() && (v.contains("mm") || v.contains("/F")))
317            })
318            .or_else(|| {
319                find_tag_value(tags, "LensType").filter(|v| !v.is_empty() && v.contains("mm"))
320            });
321        if let Some(lm) = lens_val {
322            // Apply PrintConv: s/ - /-/ (remove spaces around dash), etc.
323            let lens_id = lm
324                .replace(" - ", "-")
325                .replace("mmF", "mm F")
326                .replace("/F", "mm F");
327            composite.push(mk_composite("LensID", "Lens ID", Value::String(lens_id)));
328        }
329    }
330
331    // Nikon SerialNumber (from InternalSerialNumber) - Nikon only
332    {
333        let make = find_tag_value(tags, "Make").unwrap_or_default();
334        if make.to_uppercase().contains("NIKON")
335            && maker_note_module_used(tags, "Nikon")
336            && find_tag(tags, "SerialNumber").is_none()
337        {
338            if let Some(sn) = find_tag_value(tags, "InternalSerialNumber") {
339                composite.push(mk_composite(
340                    "SerialNumber",
341                    "Serial Number",
342                    Value::String(sn),
343                ));
344            }
345        }
346    }
347
348    // LensSpec — Nikon-only composite (handled in Nikon section below)
349
350    // Nikon-specific composites
351    {
352        let make = find_tag_value(tags, "Make").unwrap_or_default();
353        // Nikon.pm registers these composites only when it is loaded, i.e. only
354        // when a Nikon maker note was really decoded (Nikon.pm line 13525).
355        let nikon_module = maker_note_module_used(tags, "Nikon");
356        if make.to_uppercase().contains("NIKON") && nikon_module {
357            // AutoFocus from FocusMode
358            if let Some(fm_tag) = tags.iter().find(|t| t.name == "FocusMode") {
359                if find_tag(&composite, "AutoFocus").is_none() {
360                    let af = if fm_tag.print_value.contains("Manual") {
361                        "Off"
362                    } else {
363                        "On"
364                    };
365                    composite.push(mk_composite(
366                        "AutoFocus",
367                        "Auto Focus",
368                        Value::String(af.into()),
369                    ));
370                }
371            }
372            // LensSpec from Lens + the Nikon lens-type code (e.g. " G"/" D"), matching
373            // Nikon's ConvertLensSpec which suffixes the type after the focal/aperture spec.
374            if find_tag(tags, "LensSpec").is_none() {
375                if let Some(lens) = find_tag_value(tags, "Lens") {
376                    if !lens.is_empty() {
377                        let mut spec = lens;
378                        if let Some(lt) = find_tag_value(tags, "LensType") {
379                            let lt = lt.trim();
380                            // Append only the short Nikon DecodeBits codes (not "AF"/numeric).
381                            if !lt.is_empty()
382                                && lt != "AF"
383                                && lt
384                                    .chars()
385                                    .all(|c| c.is_ascii_uppercase() || c == ' ' || c == '-')
386                            {
387                                spec = format!("{} {}", spec, lt);
388                            }
389                        }
390                        composite.push(mk_composite("LensSpec", "Lens Spec", Value::String(spec)));
391                    }
392                }
393            }
394            // Nikon LensID composite: construct 8-byte key from LensData fields
395            // Require: LensIDNumber + MinFocalLength (to distinguish from generic LensType fallback)
396            if !composite.iter().any(|t| t.name == "LensID")
397                && find_tag(tags, "LensIDNumber").is_some()
398                && find_tag(tags, "MinFocalLength").is_some()
399            {
400                if let Some(lens_id) = compute_nikon_lens_id(tags) {
401                    composite.push(mk_composite("LensID", "Lens ID", Value::String(lens_id)));
402                }
403            }
404        }
405    }
406
407    // Kodak DateCreated composite (from YearCreated+MonthDayCreated)
408    if let (Some(year), Some(md)) = (
409        find_tag_value(tags, "YearCreated"),
410        find_tag_value(tags, "MonthDayCreated"),
411    ) {
412        if !year.is_empty() && !md.is_empty() {
413            composite.push(mk_composite(
414                "DateCreated",
415                "Date Created",
416                Value::String(format!("{}:{}", year, md)),
417            ));
418        }
419    }
420
421    // Panasonic AdvancedSceneMode composite (PanasonicRaw::Composite — only for RW2 files)
422    // Only fire when PanasonicRaw-specific tags exist (e.g. SensorTopBorder)
423    if find_tag(tags, "AdvancedSceneMode").is_none() && find_tag(tags, "SensorTopBorder").is_some()
424    {
425        if let Some(adv) = compute_panasonic_advanced_scene_mode(tags) {
426            composite.push(adv);
427        }
428    }
429
430    // CFAPattern composite: convert CFAPattern2 + CFARepeatPatternDim to readable format
431    // e.g., "0 1 1 2" with dim "2 2" → "[Red,Green][Green,Blue]"
432    //
433    // `%Exif::Composite` CFAPattern (Exif.pm:5221) only `Require`s
434    // CFARepeatPatternDim and CFAPattern2; an EXIF 0xa302 CFAPattern in the same
435    // file does not suppress it. Both are then stored, and the Composite one —
436    // added last, at the normal priority — is the one ExifTool reports.
437    {
438        if let (Some(pat), Some(dim)) = (
439            find_tag_value(tags, "CFAPattern2"),
440            find_tag_value(tags, "CFARepeatPatternDim"),
441        ) {
442            let dims: Vec<usize> = dim
443                .split([',', ' '])
444                .filter_map(|s| s.trim().parse().ok())
445                .collect();
446            let vals: Vec<u8> = pat
447                .split([',', ' '])
448                .filter_map(|s| s.trim().parse().ok())
449                .collect();
450            if dims.len() == 2 && dims[0] > 0 && dims[1] > 0 && vals.len() >= dims[0] * dims[1] {
451                let color = |v: u8| match v {
452                    0 => "Red",
453                    1 => "Green",
454                    2 => "Blue",
455                    3 => "Cyan",
456                    4 => "Magenta",
457                    5 => "Yellow",
458                    6 => "White",
459                    _ => "?",
460                };
461                let mut s = String::new();
462                for row in 0..dims[1] {
463                    s.push('[');
464                    for col in 0..dims[0] {
465                        if col > 0 {
466                            s.push(',');
467                        }
468                        s.push_str(color(vals[row * dims[0] + col]));
469                    }
470                    s.push(']');
471                }
472                composite.push(mk_composite("CFAPattern", "CFA Pattern", Value::String(s)));
473            }
474        }
475    }
476
477    // ThumbnailTIFF composite: rebuild TIFF from IFD tags
478    // Requires SubfileType=1 (reduced-resolution) + Compression=1 (uncompressed)
479    if find_tag(tags, "ThumbnailTIFF").is_none() {
480        if let Some(t) = build_thumbnail_tiff(tags) {
481            composite.push(t);
482        }
483    }
484
485    composite
486}
487
488/// Build the ThumbnailTIFF composite tag, or `None` when the file has no
489/// reduced-resolution uncompressed IFD to rebuild it from.
490///
491/// Exposed so a format reader that prunes duplicate IFD tags (IIQ) can build the
492/// composite while the IFDs are still intact.
493pub(crate) fn build_thumbnail_tiff(tags: &[Tag]) -> Option<Tag> {
494    let (tiff_data, grp0, grp1) = compute_thumbnail_tiff(tags)?;
495    let size = tiff_data.len();
496    Some(Tag {
497        id: TagId::Text("ThumbnailTIFF".into()),
498        name: "ThumbnailTIFF".into(),
499        description: "Thumbnail TIFF".into(),
500        group: TagGroup {
501            // Exif.pm:6206 `($grp0, $grp1) = $et->GetGroup($key)` — the composite
502            // inherits the groups of the SubfileType instance it was rebuilt from,
503            // not the generic Composite group.
504            family0: grp0,
505            family1: grp1,
506            family2: "Preview".into(),
507            family3: "Main".into(),
508        },
509        raw_value: Value::Binary(tiff_data),
510        print_value: format!("(Binary data {} bytes, use -b option to extract)", size),
511        priority: 0,
512    })
513}
514
515/// Build a minimal TIFF file from IFD thumbnail tags.
516///
517/// Port of `Image::ExifTool::Exif::RebuildTIFF` (Exif.pm:6139): it walks every
518/// SubfileType instance, keeps the one equal to 1 (reduced-resolution image,
519/// Exif.pm:6150), remembers that instance's family-1 group
520/// (`my $grp = $et->GetGroup($key, 1)`, Exif.pm:6151) and reads every other
521/// required tag from that same group (`$et->FindValue($_, $grp)`, Exif.pm:6159).
522/// Returns the rebuilt TIFF plus the family-0/1 groups the composite inherits
523/// (Exif.pm:6206).
524fn compute_thumbnail_tiff(tags: &[Tag]) -> Option<(Vec<u8>, String, String)> {
525    // Find the SubfileType instance whose value is 1 (reduced-resolution image).
526    let subfile = tags.iter().find(|t| {
527        t.name == "SubfileType"
528            && t.raw_value
529                .as_u64()
530                .or_else(|| {
531                    if t.print_value.contains("Reduced") {
532                        Some(1)
533                    } else {
534                        t.print_value.trim().parse().ok()
535                    }
536                })
537                .is_some_and(|v| v == 1)
538    })?;
539    let grp0 = subfile.group.family0.clone();
540    let grp1 = subfile.group.family1.clone();
541    // Every other required tag comes from that same family-1 group.
542    let scoped: Vec<Tag> = tags
543        .iter()
544        .filter(|t| t.group.family1 == grp1)
545        .cloned()
546        .collect();
547    let tags: &[Tag] = &scoped;
548
549    // Compression must be 1 (uncompressed)
550    let comp = find_tag(tags, "Compression")?;
551    let comp_val = comp.raw_value.as_u64().or_else(|| {
552        if comp.print_value.contains("Uncompressed") {
553            Some(1)
554        } else {
555            comp.print_value.trim().parse().ok()
556        }
557    })?;
558    if comp_val != 1 {
559        return None;
560    }
561
562    // Get required tags
563    let _strip_off = find_tag(tags, "StripOffsets")?.raw_value.as_u64()? as u32;
564    let strip_len = find_tag(tags, "StripByteCounts")?.raw_value.as_u64()? as u32;
565    let bps_str = find_tag_value(tags, "BitsPerSample").unwrap_or_default();
566    let bps_vals: Vec<u16> = bps_str
567        .split([',', ' '])
568        .filter_map(|s| s.trim().parse().ok())
569        .collect();
570    if bps_vals.is_empty() {
571        return None;
572    }
573    let spp = find_tag(tags, "SamplesPerPixel")?.raw_value.as_u64()? as u16;
574    let rps = find_tag(tags, "RowsPerStrip")?.raw_value.as_u64()? as u32;
575    let photo = find_tag(tags, "PhotometricInterpretation")?
576        .raw_value
577        .as_u64()
578        .or_else(|| {
579            if find_tag_value(tags, "PhotometricInterpretation")?.contains("RGB") {
580                Some(2)
581            } else {
582                Some(1)
583            }
584        })? as u16;
585    let _planar = find_tag(tags, "PlanarConfiguration")
586        .and_then(|t| t.raw_value.as_u64())
587        .unwrap_or(1) as u16;
588    let orient = find_tag(tags, "Orientation")
589        .and_then(|t| t.raw_value.as_u64())
590        .unwrap_or(1) as u16;
591
592    // Calculate image dimensions from strip data
593    let bytes_per_pixel: u32 = bps_vals.iter().map(|&b| (b as u32).div_ceil(8)).sum();
594    if bytes_per_pixel == 0 || rps == 0 {
595        return None;
596    }
597    let row_bytes = strip_len / rps;
598    if row_bytes == 0 {
599        return None;
600    }
601    let w = row_bytes / bytes_per_pixel;
602    let h = rps;
603
604    // Build TIFF IFD entries (little-endian)
605    let num_entries: u16 = 15;
606    let ifd_size = 2 + num_entries as usize * 12 + 4;
607    let data_offset = 8 + ifd_size; // after TIFF header + IFD
608                                    // BitsPerSample data (if > 1 sample)
609    let bps_data_offset = data_offset;
610    let bps_data_size = if spp > 1 { spp as usize * 2 } else { 0 };
611    let resolution_offset = bps_data_offset + bps_data_size;
612    let strip_data_offset = resolution_offset + 16; // 2 rational values (8 bytes each)
613    let total_size = strip_data_offset + strip_len as usize;
614
615    let mut tiff = Vec::with_capacity(total_size);
616
617    // TIFF header (little-endian)
618    tiff.extend_from_slice(b"II");
619    tiff.extend_from_slice(&42u16.to_le_bytes());
620    tiff.extend_from_slice(&8u32.to_le_bytes()); // IFD offset
621
622    // IFD
623    tiff.extend_from_slice(&num_entries.to_le_bytes());
624
625    let add_entry = |tiff: &mut Vec<u8>, tag: u16, dtype: u16, count: u32, value: u32| {
626        tiff.extend_from_slice(&tag.to_le_bytes());
627        tiff.extend_from_slice(&dtype.to_le_bytes());
628        tiff.extend_from_slice(&count.to_le_bytes());
629        tiff.extend_from_slice(&value.to_le_bytes());
630    };
631
632    add_entry(&mut tiff, 0x00FE, 4, 1, 0); // SubfileType = 0
633    add_entry(&mut tiff, 0x0100, 3, 1, w); // ImageWidth
634    add_entry(&mut tiff, 0x0101, 3, 1, h); // ImageHeight
635    if spp == 1 {
636        add_entry(&mut tiff, 0x0102, 3, 1, bps_vals[0] as u32); // BitsPerSample
637    } else {
638        add_entry(&mut tiff, 0x0102, 3, spp as u32, bps_data_offset as u32);
639    }
640    add_entry(&mut tiff, 0x0103, 3, 1, 1); // Compression = Uncompressed
641    add_entry(&mut tiff, 0x0106, 3, 1, photo as u32); // PhotometricInterpretation
642    add_entry(&mut tiff, 0x0111, 4, 1, strip_data_offset as u32); // StripOffsets
643    add_entry(&mut tiff, 0x0112, 3, 1, orient as u32); // Orientation
644    add_entry(&mut tiff, 0x0115, 3, 1, spp as u32); // SamplesPerPixel
645    add_entry(&mut tiff, 0x0116, 4, 1, h); // RowsPerStrip
646    add_entry(&mut tiff, 0x0117, 4, 1, strip_len); // StripByteCounts
647    add_entry(&mut tiff, 0x011A, 5, 1, resolution_offset as u32); // XResolution
648    add_entry(&mut tiff, 0x011B, 5, 1, (resolution_offset + 8) as u32); // YResolution
649    add_entry(&mut tiff, 0x011C, 3, 1, _planar as u32); // PlanarConfiguration
650    add_entry(&mut tiff, 0x0128, 3, 1, 2); // ResolutionUnit = inches
651
652    // Next IFD offset = 0 (no more IFDs)
653    tiff.extend_from_slice(&0u32.to_le_bytes());
654
655    // BitsPerSample data (if multiple samples)
656    if spp > 1 {
657        for &b in &bps_vals {
658            tiff.extend_from_slice(&b.to_le_bytes());
659        }
660    }
661
662    // Resolution data: 72/1 for both X and Y
663    tiff.extend_from_slice(&72u32.to_le_bytes());
664    tiff.extend_from_slice(&1u32.to_le_bytes());
665    tiff.extend_from_slice(&72u32.to_le_bytes());
666    tiff.extend_from_slice(&1u32.to_le_bytes());
667
668    // Strip data — we need the actual image bytes from the original file
669    // We don't have access to the original file data here, so pad with zeros
670    // This matches Perl's behavior for the tag NAME (the value is binary data)
671    tiff.resize(total_size, 0);
672
673    Some((tiff, grp0, grp1))
674}
675
676/// The tag a composite's `Require`/`Desire` entry names.
677///
678/// The lookup is case-SENSITIVE, as ExifTool's is: a `Require` entry names a
679/// tag key, and `$$self{VALUE}{ISO}` is not `$$self{VALUE}{Iso}`. That is what
680/// keeps Composite:LightValue out of an ExifTool-written RDF/XML dump: the
681/// `ExifIFD:ISO` property it carries has no lowercase letter, and GetXMPTagID
682/// rewrites such a name — "all uppercase is ugly, so convert it" — to `Iso`
683/// unless the namespace's table already holds it (XMP.pm lines 3040-3050), so
684/// nothing is called ISO for the Require to find.
685fn find_tag<'a>(tags: &'a [Tag], name: &str) -> Option<&'a Tag> {
686    // Composites read ExifTool's primary (unnumbered) tag key. A sub-document tag
687    // never becomes that key: FoundTag only lets the incoming tag take it over
688    // when it carries no DOC_NUM or the same one (ExifTool.pm:9564). So the main
689    // document wins outright, and only if it has no instance do sub-documents
690    // compete.
691    let pick = |main_only: bool| {
692        tags.iter()
693            .filter(|t| t.name == name)
694            .filter(|t| !main_only || t.group.family3 == "Main")
695            // Prefer the highest-priority tag (first among ties) so composites use
696            // the same value ExifTool's priority dedup would surface, not merely
697            // the first extracted.
698            .fold(None, |best: Option<&Tag>, t| match best {
699                Some(b) if b.priority_rank() >= t.priority_rank() => Some(b),
700                _ => Some(t),
701            })
702    };
703    pick(true).or_else(|| pick(false))
704}
705
706/// Whether a manufacturer's maker-note module was actually used to read this
707/// file.
708///
709/// Perl registers a maker-note module's composites with `AddCompositeTags` at
710/// module load time — `Image::ExifTool::AddCompositeTags('Image::ExifTool::Nikon')`
711/// (Nikon.pm line 13525) runs when `Nikon.pm` is required, which only happens
712/// when a Nikon maker note is actually decoded. A file that never loads the
713/// module therefore never gets its composites, whatever groups its tags carry.
714///
715/// Our engine keeps every composite resident, so the same question has to be
716/// asked of the tag list. A maker-note reader keys every entry it decodes by the
717/// tag NUMBER it read it at, so a numeric tag id under `MakerNotes:<module>` is
718/// exactly the trace of that module having run. Groups alone will not do: an
719/// ExifTool-written RDF/XML dump restores `MakerNotes:Nikon` on properties that
720/// were read from an XMP sidecar (XMP.pm lines 3599-3614), and reading a sidecar
721/// loads no maker-note module at all — those properties are keyed by their XMP
722/// property path, never by a number.
723fn maker_note_module_used(tags: &[Tag], module: &str) -> bool {
724    tags.iter().any(|t| {
725        t.group.family0 == "MakerNotes"
726            && t.group.family1 == module
727            && matches!(t.id, TagId::Numeric(_))
728    })
729}
730
731fn find_tag_in_group<'a>(tags: &'a [Tag], name: &str, group: &str) -> Option<&'a Tag> {
732    let name_lower = name.to_lowercase();
733    let group_lower = group.to_lowercase();
734    tags.iter().find(|t| {
735        t.name.to_lowercase() == name_lower
736            && (t.group.family0.to_lowercase() == group_lower
737                || t.group.family1.to_lowercase() == group_lower)
738    })
739}
740
741fn find_tag_value(tags: &[Tag], name: &str) -> Option<String> {
742    find_tag(tags, name).map(|t| t.print_value.clone())
743}
744
745fn find_tag_f64(tags: &[Tag], name: &str) -> Option<f64> {
746    find_tag(tags, name).and_then(|t| {
747        // Try direct conversion first
748        t.raw_value.as_f64().or_else(|| {
749            // For lists (e.g., ISO: 0, 200), take last non-zero value
750            if let Value::List(items) = &t.raw_value {
751                items.iter().rev().find_map(|v| {
752                    let f = v.as_f64()?;
753                    if f > 0.0 {
754                        Some(f)
755                    } else {
756                        None
757                    }
758                })
759            } else {
760                // Try parsing from print value (strip units like "mm", "m", etc.)
761                t.print_value
762                    .split(',')
763                    .next_back()
764                    .and_then(|s| {
765                        let s = s.trim();
766                        // Try direct parse first, then strip common suffixes
767                        s.parse::<f64>()
768                            .ok()
769                            .or_else(|| s.trim_end_matches(" mm").trim().parse::<f64>().ok())
770                            .or_else(|| s.trim_end_matches(" m").trim().parse::<f64>().ok())
771                            .or_else(|| s.split_whitespace().next()?.parse::<f64>().ok())
772                    })
773                    .filter(|&v| v > 0.0)
774            }
775        })
776    })
777}
778
779/// GPS Composite `GPSLatitude` / `GPSLongitude` (GPS.pm:367-405).
780///
781/// `Require => { 0 => 'GPS:GPSLatitude', 1 => 'GPS:GPSLatitudeRef' }`,
782/// `ValueConv => '$val[1] =~ /^S/i ? -$val[0] : $val[0]'` and
783/// `PrintConv => ToDMS($self, $val, 1, "N")` — the hemisphere reference is folded
784/// into the Composite's printed value, not into the GPS tag's. `Avoid => 1` with
785/// an explicit `Priority => 1` puts it at the normal priority, so being built
786/// last it wins the duplicate competition against the GPS tag of the same name.
787pub fn gps_coordinates(tags: &[Tag]) -> Vec<Tag> {
788    let mut out = Vec::new();
789    for (coord, reftag, pos_ref, neg_ref) in [
790        ("GPSLatitude", "GPSLatitudeRef", 'N', 'S'),
791        ("GPSLongitude", "GPSLongitudeRef", 'E', 'W'),
792    ] {
793        // `Require` is satisfied by the mere presence of both tags, whatever
794        // their values.
795        let Some(refv) = tags
796            .iter()
797            .find(|t| t.name == reftag && t.group.family1 == "GPS")
798            .map(|t| t.raw_value.to_display_string())
799        else {
800            continue;
801        };
802        let Some(src) = tags
803            .iter()
804            .find(|t| t.name == coord && t.group.family1 == "GPS")
805        else {
806            continue;
807        };
808        // ValueConv negates on a South/West reference, and ToDMS then derives the
809        // printed hemisphere letter from that sign (GPS.pm:1063-1080). ToDMS
810        // returns an empty value untouched, without a reference, when there is
811        // nothing to convert (GPS.pm:1057-1061).
812        let negative = refv
813            .chars()
814            .next()
815            .is_some_and(|c| c.eq_ignore_ascii_case(&neg_ref));
816        let print = if src.print_value.is_empty() {
817            String::new()
818        } else {
819            format!(
820                "{} {}",
821                src.print_value,
822                if negative { neg_ref } else { pos_ref }
823            )
824        };
825        let mut t = mk_composite_raw(
826            coord,
827            src.description.clone().as_str(),
828            src.raw_value.clone(),
829            print,
830        );
831        t.group.family2 = "Location".into();
832        out.push(t);
833    }
834    out
835}
836fn compute_gps_position(tags: &[Tag]) -> Option<Tag> {
837    // GPS.pm:346-355 — GPSPosition `Require`s `Composite:GPSLatitude` and
838    // `Composite:GPSLongitude`, i.e. the ref-bearing values, not the plain GPS
839    // ones.
840    let composite_coord = |name: &str| -> Option<&Tag> {
841        tags.iter()
842            .find(|t| t.name == name && t.group.family0 == "Composite")
843            .or_else(|| find_tag(tags, name))
844    };
845    let lat_tag = composite_coord("GPSLatitude")?;
846    let lon_tag = composite_coord("GPSLongitude")?;
847    // GPSLatitude/GPSLongitude already carry ExifTool's "D deg M' S\" REF" print value.
848    let (lat, lon) = (lat_tag.print_value.clone(), lon_tag.print_value.clone());
849
850    if lat.is_empty() || lon.is_empty() || lat.contains("undef") || lon.contains("undef") {
851        return None;
852    }
853
854    Some(mk_composite(
855        "GPSPosition",
856        "GPS Position",
857        Value::String(format!("{}, {}", lat, lon)),
858    ))
859}
860
861fn compute_gps_altitude(tags: &[Tag]) -> Option<Tag> {
862    let alt = find_tag(tags, "GPSAltitude")?;
863    let alt_ref = find_tag_value(tags, "GPSAltitudeRef").unwrap_or_default();
864
865    let meters = alt.raw_value.as_f64()?;
866    // Perl Composite GPSAltitude: int($val*10)/10 . " m " . (Above|Below) . " Sea Level".
867    let below = alt_ref.contains("Below") || alt_ref.trim() == "1";
868    let trunc = (meters.abs() * 10.0).trunc() / 10.0;
869    let pos = if below { "Below" } else { "Above" };
870
871    let mut t = mk_composite(
872        "GPSAltitude",
873        "GPS Altitude",
874        Value::String(format!(
875            "{} m {} Sea Level",
876            crate::value::format_g15(trunc),
877            pos
878        )),
879    );
880    // ExifTool's Composite GPSAltitude overrides the plain EXIF GPSAltitude tag.
881    t.priority = 1;
882    Some(t)
883}
884
885/// Sony::Composite FocusDistance2, from the focus position and the 35 mm
886/// equivalent focal length.
887///
888/// Sony.pm carries a note that the formula may be wrong -- the embedded value
889/// looks like a magnification rather than a distance -- but the point here is
890/// to print what ExifTool prints.
891fn compute_sony_focus_distance2(tags: &[Tag]) -> Option<Tag> {
892    let pos = tags
893        .iter()
894        .find(|t| t.name == "FocusPosition2" && t.group.family1 == "Sony")
895        .and_then(|t| t.raw_value.as_f64())?;
896    let focal35 = find_tag_f64(tags, "FocalLengthIn35mmFormat")?;
897    if pos == 0.0 {
898        return None;
899    }
900    let print = if pos >= 255.0 {
901        "inf".to_string()
902    } else {
903        let metres = (2.0_f64.powf(pos / 16.0 - 5.0) + 1.0) * focal35 / 1000.0;
904        crate::tags::conv_expr::eval(
905            "sprintf(\"%.4g m\", $val)",
906            &crate::tags::conv_expr::Val::Num(metres),
907        )?
908        .as_string()
909    };
910    Some(mk_composite(
911        "FocusDistance2",
912        "Focus Distance 2",
913        Value::String(print),
914    ))
915}
916
917fn compute_shutter_speed(tags: &[Tag]) -> Option<Tag> {
918    let et = find_tag(tags, "ExposureTime")?;
919    // Perl: Composite ShutterSpeed PrintConv = PrintExposureTime($val) applied to the
920    // raw ExposureTime, regardless of how ExposureTime itself is displayed.
921    let print = et
922        .raw_value
923        .as_f64()
924        .map(crate::tags::canon_sub::print_exposure_time)
925        .unwrap_or_else(|| et.print_value.clone());
926    Some(mk_composite(
927        "ShutterSpeed",
928        "Shutter Speed",
929        Value::String(print),
930    ))
931}
932
933/// Perl: Aperture = FNumber || ApertureValue
934fn compute_aperture(tags: &[Tag]) -> Option<Tag> {
935    let val = find_tag_f64(tags, "FNumber")
936        .or_else(|| find_tag_f64(tags, "ApertureValue").map(|av| 2.0_f64.powf(av / 2.0)))?;
937    if val <= 0.0 {
938        return None;
939    }
940    // PrintFNumber: "%.2f" below f/1, else "%.1f".
941    let print = if val < 1.0 {
942        format!("{:.2}", val)
943    } else {
944        format!("{:.1}", val)
945    };
946    Some(mk_composite("Aperture", "Aperture", Value::String(print)))
947}
948
949fn compute_image_size(tags: &[Tag]) -> Option<Tag> {
950    // Perl ImageSize ValueConv: RawImageCroppedSize (RAF) wins; ExifImageWidth/Height for
951    // CR2/IIQ/EIP; otherwise the native ImageWidth/Height.
952    if let Some(rcs) = find_tag_value(tags, "RawImageCroppedSize") {
953        let v = rcs.trim().replace(' ', "x");
954        if !v.is_empty() {
955            return Some(mk_composite("ImageSize", "Image Size", Value::String(v)));
956        }
957    }
958    let ft = find_tag(tags, "FileType")
959        .map(|t| t.print_value.clone())
960        .unwrap_or_default();
961    if matches!(ft.as_str(), "CR2" | "IIQ" | "EIP") {
962        let ew = find_tag(tags, "ExifImageWidth").and_then(|t| {
963            t.raw_value
964                .as_u64()
965                .or_else(|| t.print_value.trim().parse().ok())
966        });
967        let eh = find_tag(tags, "ExifImageHeight").and_then(|t| {
968            t.raw_value
969                .as_u64()
970                .or_else(|| t.print_value.trim().parse().ok())
971        });
972        if let (Some(w), Some(h)) = (ew, eh) {
973            return Some(mk_composite(
974                "ImageSize",
975                "Image Size",
976                Value::String(format!("{}x{}", w, h)),
977            ));
978        }
979    }
980    let width = find_tag(tags, "ImageWidth").and_then(|t| {
981        t.raw_value
982            .as_u64()
983            .or_else(|| t.print_value.trim().parse().ok())
984    });
985    let height = find_tag(tags, "ImageHeight").and_then(|t| {
986        t.raw_value
987            .as_u64()
988            .or_else(|| t.print_value.trim().parse().ok())
989    });
990
991    let (width, height) = (width?, height?);
992
993    Some(mk_composite(
994        "ImageSize",
995        "Image Size",
996        Value::String(format!("{}x{}", width, height)),
997    ))
998}
999
1000/// Perl: Require => 'ImageSize', ValueConv => 'my @d = ($val =~ /\d+/g); $d[0] * $d[1] / 1000000'
1001fn compute_megapixels(tags: &[Tag]) -> Option<Tag> {
1002    // Use ImageSize composite (already computed) like Perl does
1003    let sz = find_tag_value(tags, "ImageSize").or_else(|| {
1004        let w = find_tag(tags, "ImageWidth")?;
1005        let h = find_tag(tags, "ImageHeight")?;
1006        let wv = w
1007            .raw_value
1008            .as_u64()
1009            .or_else(|| w.print_value.parse().ok())?;
1010        let hv = h
1011            .raw_value
1012            .as_u64()
1013            .or_else(|| h.print_value.parse().ok())?;
1014        Some(format!("{}x{}", wv, hv))
1015    })?;
1016
1017    let nums: Vec<f64> = sz
1018        .split(|c: char| !c.is_ascii_digit())
1019        .filter_map(|s| s.parse().ok())
1020        .collect();
1021    if nums.len() < 2 {
1022        return None;
1023    }
1024
1025    let mp = nums[0] * nums[1] / 1_000_000.0;
1026    // Perl: sprintf("%.*f", ($val >= 1 ? 1 : ($val >= 0.001 ? 3 : 6)), $val)
1027    let fmt = if mp >= 1.0 {
1028        format!("{:.1}", mp)
1029    } else if mp >= 0.001 {
1030        format!("{:.3}", mp)
1031    } else {
1032        format!("{:.6}", mp)
1033    };
1034
1035    Some(mk_composite("Megapixels", "Megapixels", Value::String(fmt)))
1036}
1037
1038/// Perl: LV = 2*log2(Aperture) - log2(ShutterSpeed) - log2(ISO/100)
1039/// Uses composites Aperture, ShutterSpeed, ISO (not raw FNumber/ExposureTime)
1040fn compute_light_value(tags: &[Tag]) -> Option<Tag> {
1041    // Aperture from composite or FNumber or ApertureValue
1042    let aperture = find_tag_f64(tags, "FNumber")
1043        .or_else(|| find_tag_f64(tags, "ApertureValue").map(|av| 2.0_f64.powf(av / 2.0)))?;
1044
1045    // ShutterSpeed from ExposureTime or ShutterSpeedValue
1046    let shutter = find_tag_f64(tags, "ExposureTime")
1047        .or_else(|| find_tag_f64(tags, "ShutterSpeedValue").map(|sv| 2.0_f64.powf(-sv)))
1048        .or_else(|| {
1049            // Parse from ShutterSpeed composite print value like "1/60 s"
1050            find_tag_value(tags, "ShutterSpeed").and_then(|s| {
1051                let s = s.trim_end_matches(" s").trim();
1052                if s.contains('/') {
1053                    let parts: Vec<&str> = s.split('/').collect();
1054                    let n: f64 = parts[0].parse().ok()?;
1055                    let d: f64 = parts[1].parse().ok()?;
1056                    Some(n / d)
1057                } else {
1058                    s.parse().ok()
1059                }
1060            })
1061        })?;
1062
1063    let iso = find_tag_f64(tags, "ISO")?;
1064
1065    if shutter <= 0.0 || iso <= 0.0 || aperture <= 0.0 {
1066        return None;
1067    }
1068
1069    // LV = 2*log2(Aperture) - log2(ShutterSpeed) - log2(ISO/100)
1070    let lv = 2.0 * aperture.log2() - shutter.log2() - (iso / 100.0).log2();
1071
1072    Some(mk_composite(
1073        "LightValue",
1074        "Light Value",
1075        Value::String(format!("{:.1}", lv)),
1076    ))
1077}
1078
1079/// Compute 35mm equivalent focal length and scale factor.
1080/// Canon CalcSensorDiag (Canon.pm): the sensor diagonal in mm, derived from the
1081/// rational denominators of FocalPlaneX/YResolution (sensor size in inches × 1000).
1082fn canon_sensor_diag(tags: &[Tag]) -> Option<f64> {
1083    let make = find_tag_value(tags, "Make").unwrap_or_default();
1084    if !make.contains("Canon") {
1085        return None;
1086    }
1087    let rat = |name: &str| -> Option<(u32, u32)> {
1088        match find_tag(tags, name)?.raw_value {
1089            Value::URational(n, d) => Some((n, d)),
1090            _ => None,
1091        }
1092    };
1093    let (xn, xd) = rat("FocalPlaneXResolution")?;
1094    let (yn, yd) = rat("FocalPlaneYResolution")?;
1095    // Verify the Canon assumptions (numerators = px×1000, denominators = inches×1000).
1096    if xn % 1000 == 0
1097        && yn % 1000 == 0
1098        && xn >= 640_000
1099        && yn >= 480_000
1100        && xn < 10_000_000
1101        && yn < 10_000_000
1102        && (61..1500).contains(&xd)
1103        && (61..1000).contains(&yd)
1104        && xd != yd
1105    {
1106        return Some(((xd as f64).powi(2) + (yd as f64).powi(2)).sqrt() * 0.0254);
1107    }
1108    None
1109}
1110
1111fn compute_35efl(tags: &[Tag]) -> Option<Vec<Tag>> {
1112    let fl = find_tag_f64(tags, "FocalLength")?;
1113    if fl <= 0.0 {
1114        return None;
1115    }
1116
1117    let mut result = Vec::new();
1118
1119    // Perl: sqrt(36*36+24*24) — full precision (43.266615305567875), not truncated.
1120    let diag35 = (36.0_f64 * 36.0 + 24.0 * 24.0).sqrt();
1121
1122    // Compute scale factor (Perl: CalcScaleFactor35efl)
1123    // Sources: FocalLengthIn35mmFormat, FocalPlaneDiagonal, FocalPlaneResolution
1124    let scale = if let Some(fl35) = find_tag_f64(tags, "FocalLengthIn35mmFormat") {
1125        if fl35 > 0.0 {
1126            fl35 / fl
1127        } else {
1128            return None;
1129        }
1130    } else if let Some(diag) = canon_sensor_diag(tags) {
1131        // Canon CalcSensorDiag takes precedence over FocalPlaneDiagonal/Size (Canon.pm).
1132        diag35 / diag
1133    } else if let Some(diag) = find_tag_f64(tags, "FocalPlaneDiagonal").or_else(|| {
1134        find_tag_value(tags, "FocalPlaneDiagonal")
1135            .and_then(|s| s.split_whitespace().next()?.parse().ok())
1136    }) {
1137        // Sanity check: diagonal must be reasonable (1-100mm)
1138        if diag > 1.0 && diag < 100.0 {
1139            diag35 / diag
1140        } else {
1141            return None;
1142        }
1143    } else if let Some(fpxs) = find_tag_f64(tags, "FocalPlaneXSize")
1144        .filter(|&v| v < 100.0) // Skip raw U16 values (914 etc.)
1145        .or_else(|| {
1146            find_tag_value(tags, "FocalPlaneXSize")
1147                .and_then(|s| s.split_whitespace().next()?.parse::<f64>().ok())
1148        })
1149    {
1150        // FocalPlaneXSize/YSize path (mm values)
1151        let fpys = find_tag_f64(tags, "FocalPlaneYSize")
1152            .filter(|&v| v < 100.0)
1153            .or_else(|| {
1154                find_tag_value(tags, "FocalPlaneYSize")
1155                    .and_then(|s| s.split_whitespace().next()?.parse::<f64>().ok())
1156            })?;
1157        let diag = (fpxs * fpxs + fpys * fpys).sqrt();
1158        if diag > 1.0 && diag < 100.0 {
1159            diag35 / diag
1160        } else {
1161            return None;
1162        }
1163    } else {
1164        // Compute from sensor size via FocalPlaneResolution
1165        let fpxr = find_tag_f64(tags, "FocalPlaneXResolution")?;
1166        // FocalPlaneYResolution defaults to X if not present (e.g., Lytro: "Y same as X")
1167        let fpyr = find_tag_f64(tags, "FocalPlaneYResolution").unwrap_or(fpxr);
1168        // Use largest available image dimensions (full sensor)
1169        let img_w = find_tag_f64(tags, "RelatedImageWidth")
1170            .or_else(|| find_tag_f64(tags, "ExifImageWidth"))
1171            .or_else(|| find_tag_f64(tags, "ImageWidth"))?;
1172        let img_h = find_tag_f64(tags, "RelatedImageHeight")
1173            .or_else(|| find_tag_f64(tags, "ExifImageHeight"))
1174            .or_else(|| find_tag_f64(tags, "ImageHeight"))?;
1175        if fpxr <= 0.0 || fpyr <= 0.0 || img_w <= 0.0 || img_h <= 0.0 {
1176            return None;
1177        }
1178
1179        let unit = find_tag_f64(tags, "FocalPlaneResolutionUnit").unwrap_or(2.0);
1180        let factor = match unit as u32 {
1181            2 => 25.4,
1182            3 => 10.0,
1183            _ => 25.4,
1184        };
1185        let sensor_w = img_w * factor / fpxr;
1186        let sensor_h = img_h * factor / fpyr;
1187        let sensor_diag = (sensor_w * sensor_w + sensor_h * sensor_h).sqrt();
1188        // Sanity check
1189        if sensor_diag <= 1.0 || sensor_diag >= 100.0 {
1190            return None;
1191        }
1192        // Aspect ratio sanity check
1193        let ratio = if sensor_w > sensor_h {
1194            sensor_w / sensor_h
1195        } else {
1196            sensor_h / sensor_w
1197        };
1198        if ratio > 3.0 {
1199            return None;
1200        }
1201        diag35 / sensor_diag
1202    };
1203
1204    let fl35_val = fl * scale;
1205
1206    result.push(mk_composite(
1207        "ScaleFactor35efl",
1208        "Scale Factor To 35 mm Equivalent",
1209        Value::String(format!("{:.1}", scale)),
1210    ));
1211    result.push(mk_composite(
1212        "FocalLength35efl",
1213        "Focal Length (35mm equivalent)",
1214        Value::String(format!(
1215            "{:.1} mm (35 mm equivalent: {:.1} mm)",
1216            fl, fl35_val
1217        )),
1218    ));
1219
1220    // CircleOfConfusion: Perl formula = sqrt(24²+36²) / (scale * 1440)
1221    let coc = (24.0_f64.powi(2) + 36.0_f64.powi(2)).sqrt() / (scale * 1440.0);
1222    result.push(mk_composite_raw(
1223        "CircleOfConfusion",
1224        "Circle of Confusion",
1225        Value::F64(coc),
1226        format!("{:.3} mm", coc),
1227    ));
1228
1229    // FOV — Perl Exif::Composite FOV: atan2(36, 2*FL*ScaleFactor*corr), with a focus-
1230    // distance correction and an optional field-width suffix.
1231    {
1232        let fd = find_tag_f64(tags, "FocusDistance").filter(|v| v.is_finite() && *v > 0.0);
1233        let mut corr = 1.0;
1234        if let Some(d_m) = fd {
1235            let d = 1000.0 * d_m - fl;
1236            if d > 0.0 {
1237                corr += fl / d;
1238            }
1239        }
1240        let fd2 = 36.0_f64.atan2(2.0 * fl * scale * corr);
1241        // ExifTool uses the literal 3.14159 (not full-precision PI) for FOV; matching
1242        // it exactly is required for value parity, so keep the approximate constant.
1243        #[allow(clippy::approx_constant)]
1244        let fov_deg = fd2 * 360.0 / 3.14159;
1245        let mut fov_str = format!("{:.1} deg", fov_deg);
1246        if let Some(d_m) = fd {
1247            if d_m > 0.0 && d_m < 10000.0 {
1248                let width = 2.0 * d_m * fd2.sin() / fd2.cos();
1249                fov_str.push_str(&format!(" ({:.2} m)", width));
1250            }
1251        }
1252        result.push(mk_composite("FOV", "Field of View", Value::String(fov_str)));
1253    }
1254
1255    // Lens + Lens35efl (Canon-specific)
1256    let make = find_tag_value(tags, "Make").unwrap_or_default();
1257    let min_fl = find_tag_f64(tags, "MinFocalLength");
1258    let max_fl = find_tag_f64(tags, "MaxFocalLength");
1259    if let (Some(min), Some(max)) = (min_fl, max_fl) {
1260        if min > 0.0 && max > 0.0 && max > min {
1261            result.push(mk_composite(
1262                "Lens",
1263                "Lens",
1264                Value::String(format!("{:.1} - {:.1} mm", min, max)),
1265            ));
1266            // Lens35efl only for Canon
1267            if make.contains("Canon") {
1268                result.push(mk_composite(
1269                    "Lens35efl",
1270                    "Lens (35mm equivalent)",
1271                    Value::String(format!(
1272                        "{:.1} - {:.1} mm (35 mm equivalent: {:.1} - {:.1} mm)",
1273                        min,
1274                        max,
1275                        min * scale,
1276                        max * scale
1277                    )),
1278                ));
1279            }
1280        }
1281    }
1282
1283    // LensID (Canon PrintLensID logic). Skip when a Nikon LensData hex lookup is
1284    // possible (LensIDNumber present) — that yields the full lens name, computed
1285    // later, not just the LensType bit-field abbreviation ("G").
1286    let nikon_lensid_possible =
1287        find_tag(tags, "LensIDNumber").is_some() && find_tag(tags, "MinFocalLength").is_some();
1288    if let Some(lt) = find_tag(tags, "LensType").filter(|_| !nikon_lensid_possible) {
1289        let raw_val = lt
1290            .raw_value
1291            .as_u64()
1292            .map(|v| v as i64)
1293            .unwrap_or_else(|| lt.raw_value.to_display_string().parse::<i64>().unwrap_or(0));
1294        let pv = lt.print_value.clone();
1295        // For LensType = -1 or 65535 ("n/a" / "Unknown"): "Unknown ShortFocal-LongFocalmm"
1296        if raw_val == -1 || raw_val == 65535 {
1297            let min_fl = find_tag_f64(tags, "MinFocalLength");
1298            let max_fl = find_tag_f64(tags, "MaxFocalLength");
1299            let lens_str = if let (Some(min), Some(max)) = (min_fl, max_fl) {
1300                if min > 0.0 && max > 0.0 && (max - min).abs() > 0.1 {
1301                    format!("Unknown {:.0}-{:.0}mm", min, max)
1302                } else if min > 0.0 {
1303                    format!("Unknown {:.0}mm", min)
1304                } else {
1305                    "Unknown".to_string()
1306                }
1307            } else {
1308                "Unknown".to_string()
1309            };
1310            result.push(mk_composite("LensID", "Lens ID", Value::String(lens_str)));
1311        } else if !pv.is_empty() && pv != "0" && pv != "n/a" {
1312            // PrintLensID: a base "… Lens (key)" may have sub-variants disambiguated
1313            // by the actual FocalLength (Pentax "3 44" → "3 44.1", Sigma 0x145 → .1).
1314            let resolved =
1315                disambiguate_lens_id(&pv, find_tag_f64(tags, "FocalLength")).unwrap_or(pv);
1316            result.push(mk_composite("LensID", "Lens ID", Value::String(resolved)));
1317        }
1318    }
1319
1320    Some(result)
1321}
1322
1323/// Parse focal range (short, long mm) from a lens name like
1324/// "Sigma AF 10-20mm F4-5.6 EX DC" → (10.0, 20.0). Prime lenses → (f, f).
1325fn lens_focal_range(name: &str) -> Option<(f64, f64)> {
1326    // Find "<num>[-<num>]mm"
1327    let mm = name.find("mm")?;
1328    let head = &name[..mm];
1329    // Take the trailing focal token (digits, '.', '-').
1330    let start = head
1331        .rfind(|c: char| !(c.is_ascii_digit() || c == '.' || c == '-'))
1332        .map(|i| i + 1)
1333        .unwrap_or(0);
1334    let tok = &head[start..];
1335    if tok.is_empty() {
1336        return None;
1337    }
1338    let mut it = tok.split('-').filter_map(|s| s.parse::<f64>().ok());
1339    let sf = it.next()?;
1340    let lf = it.next().unwrap_or(sf);
1341    if sf > 0.0 {
1342        Some((sf, lf))
1343    } else {
1344        None
1345    }
1346}
1347
1348/// PrintLensID sub-variant disambiguation for the LensType values that need it.
1349/// Returns the specific lens name when exactly the actual FocalLength selects it.
1350fn disambiguate_lens_id(base_print: &str, focal_length: Option<f64>) -> Option<String> {
1351    // (base LensType print value, [sub-variant lens names])
1352    const SUBVARIANTS: &[(&str, &[&str])] = &[
1353        (
1354            "Sigma or Tamron Lens (3 44)",
1355            &[
1356                "Sigma AF 10-20mm F4-5.6 EX DC",
1357                "Sigma 12-24mm F4.5-5.6 EX DG",
1358                "Sigma 17-70mm F2.8-4.5 DC Macro",
1359                "Sigma 18-50mm F3.5-5.6 DC",
1360                "Sigma 17-35mm F2.8-4 EX DG",
1361                "Tamron 35-90mm F4-5.6 AF",
1362                "Sigma AF 18-35mm F3.5-4.5 Aspherical",
1363            ],
1364        ),
1365        (
1366            "Sigma Lens (0x145)",
1367            &["Sigma 15-30mm F3.5-4.5 EX DG Aspherical"],
1368        ),
1369    ];
1370    let variants = SUBVARIANTS
1371        .iter()
1372        .find(|(b, _)| *b == base_print)
1373        .map(|(_, v)| *v)?;
1374    let fl = focal_length?;
1375    // Keep variants whose focal range contains the actual FocalLength (±0.5).
1376    let mut matches = variants.iter().filter(|name| {
1377        lens_focal_range(name)
1378            .map(|(sf, lf)| fl >= sf - 0.5 && fl <= lf + 0.5)
1379            .unwrap_or(false)
1380    });
1381    let first = matches.next()?;
1382    // Only disambiguate when the match is unambiguous (a single candidate).
1383    if matches.next().is_none() {
1384        Some((*first).to_string())
1385    } else {
1386        None
1387    }
1388}
1389
1390/// Build SubSec composite date.
1391/// Only emit when subsec or offset actually adds information.
1392fn make_subsec_date(
1393    tags: &[Tag],
1394    date_tag: &str,
1395    subsec_tag: &str,
1396    offset_tag: &str,
1397    output_name: &str,
1398) -> Option<Tag> {
1399    let dt = find_tag_value(tags, date_tag)?;
1400    if dt.is_empty() {
1401        return None;
1402    }
1403
1404    let subsec = find_tag_value(tags, subsec_tag).unwrap_or_default();
1405    let offset = find_tag_value(tags, offset_tag).unwrap_or_default();
1406
1407    let mut result = dt.clone();
1408    let mut modified = false;
1409
1410    // Only add subsec if date doesn't already have subseconds (contains '.')
1411    if !subsec.is_empty() && !dt.contains('.') {
1412        result = format!("{}.{}", result, subsec.trim());
1413        modified = true;
1414    }
1415    // Only add offset if date doesn't already have timezone (contains '+' or '-' after time part)
1416    if !(offset.is_empty() || dt.contains('+') || dt.len() > 10 && dt[10..].contains('-')) {
1417        result = format!("{}{}", result, offset.trim());
1418        modified = true;
1419    }
1420
1421    if !modified {
1422        return None;
1423    }
1424
1425    Some(mk_composite(
1426        output_name,
1427        output_name,
1428        Value::String(result),
1429    ))
1430}
1431
1432/// Canon-specific composites.
1433fn compute_canon_composites(tags: &[Tag]) -> Option<Vec<Tag>> {
1434    // Only if this is a Canon file
1435    let make = find_tag_value(tags, "Make").unwrap_or_default();
1436    if !make.contains("Canon") {
1437        return None;
1438    }
1439
1440    let mut result = Vec::new();
1441
1442    // DriveMode: Perl Canon::Composite — ValueConv '$val[0] ? 0 : ($val[1] ? 1 : 2)'
1443    // over (ContinuousDrive, SelfTimer), then PrintConv.
1444    if let Some(cd) = find_tag_f64(tags, "ContinuousDrive") {
1445        let st = find_tag_f64(tags, "SelfTimer").unwrap_or(0.0);
1446        let pv = if cd != 0.0 {
1447            "Continuous Shooting"
1448        } else if st != 0.0 {
1449            "Self-timer Operation"
1450        } else {
1451            "Single-frame Shooting"
1452        };
1453        result.push(mk_composite(
1454            "DriveMode",
1455            "Drive Mode",
1456            Value::String(pv.to_string()),
1457        ));
1458    }
1459
1460    // ShootingMode (Canon composite): CanonExposureMode/EasyMode with a Bulb override.
1461    // ValueConv: $val[0] ? (($val[0] eq "4" and $val[2]) ? 7 : $val[0]) : $val[1] + 10
1462    // PrintConv: $val eq "7" ? "Bulb" : ($val[0] ? $prt[0] : $prt[1])
1463    if let Some(em_tag) = find_tag(tags, "CanonExposureMode") {
1464        let em_raw = em_tag.raw_value.as_f64().map(|f| f as i64);
1465        let bulb_set = find_tag(tags, "BulbDuration")
1466            .and_then(|t| t.raw_value.as_f64())
1467            .map(|v| v != 0.0)
1468            .unwrap_or(false);
1469        let print = if em_raw.is_some_and(|v| v != 0) {
1470            if em_raw == Some(4) && bulb_set {
1471                "Bulb".to_string()
1472            } else {
1473                em_tag.print_value.clone()
1474            }
1475        } else if let Some(easy) = find_tag(tags, "EasyMode") {
1476            easy.print_value.clone()
1477        } else {
1478            em_tag.print_value.clone()
1479        };
1480        result.push(mk_composite(
1481            "ShootingMode",
1482            "Shooting Mode",
1483            Value::String(print),
1484        ));
1485    }
1486
1487    // NOTE: Canon Lens composite is handled by compute_lens_composite (main lens function)
1488    // which correctly formats it as "18.0 - 55.0 mm". Do not duplicate it here.
1489
1490    // FileNumber (Canon composite): DirectoryIndex + FileIndex → "DDD-FFFF"
1491    // Perl: sprintf("%.3d%.4d", @val), then PrintConv s/(\d+)(\d{4})/$1-$2/
1492    if let (Some(dir_idx), Some(file_idx)) = (
1493        find_tag_value(tags, "DirectoryIndex"),
1494        find_tag_value(tags, "FileIndex"),
1495    ) {
1496        if let (Ok(di), Ok(fi)) = (
1497            dir_idx.trim().parse::<i64>(),
1498            file_idx.trim().parse::<i64>(),
1499        ) {
1500            if di > 0 || fi > 0 {
1501                // Handle wrap: if FileIndex == 10000, it wraps (FileIndex=1, DirectoryIndex++)
1502                let (di2, fi2) = if fi == 10000 {
1503                    (di + 1, 1i64)
1504                } else {
1505                    (di, fi)
1506                };
1507                let combined = format!("{:03}{:04}", di2, fi2);
1508                // PrintConv: s/(\d+)(\d{4})/$1-$2/  (last 4 digits are file number)
1509                let len = combined.len();
1510                let print = if len > 4 {
1511                    format!("{}-{}", &combined[..len - 4], &combined[len - 4..])
1512                } else {
1513                    combined.clone()
1514                };
1515                let t = Tag {
1516                    id: TagId::Text("FileNumber".into()),
1517                    name: "FileNumber".into(),
1518                    description: "File Number".into(),
1519                    group: TagGroup {
1520                        family0: "Composite".into(),
1521                        family1: "Composite".into(),
1522                        family2: "Image".into(),
1523                        family3: "Main".into(),
1524                    },
1525                    raw_value: Value::String(combined),
1526                    print_value: print,
1527                    priority: 0,
1528                };
1529                result.push(t);
1530            }
1531        }
1532    }
1533
1534    // Canon ISO composite (Canon.pm:10050):
1535    // Perl: use CameraISO if numeric, else BaseISO * AutoISO / 100
1536    // Priority => 0 only decides which entry survives the name-keyed collapse, so
1537    // the composite still has to be built when the Duplicates option is on — Perl
1538    // then reports it next to EXIF:ISO.
1539    if crate::metadata::exif::keep_duplicates() || find_tag(tags, "ISO").is_none() {
1540        let camera_iso_str = find_tag_value(tags, "CameraISO");
1541        let iso_val = camera_iso_str
1542            .as_deref()
1543            .and_then(|ci| ci.trim().parse::<f64>().ok())
1544            .filter(|&v| v > 0.0);
1545        let iso = iso_val.or_else(|| {
1546            let base = find_tag_f64(tags, "BaseISO")?;
1547            let auto = find_tag_f64(tags, "AutoISO")?;
1548            if base > 0.0 && auto > 0.0 {
1549                Some(base * auto / 100.0)
1550            } else {
1551                None
1552            }
1553        });
1554        if let Some(iso_v) = iso {
1555            result.push(mk_composite(
1556                "ISO",
1557                "ISO",
1558                Value::String(format!("{:.0}", iso_v)),
1559            ));
1560        }
1561    }
1562
1563    // Canon FlashType composite:
1564    // Perl: Require FlashBits; RawConv: suppress if FlashBits==0;
1565    //        ValueConv: FlashBits & (1<<14) ? 1 : 0
1566    if let Some(flash_bits_tag) = find_tag(tags, "FlashBits") {
1567        let fb_raw = flash_bits_tag
1568            .raw_value
1569            .as_u64()
1570            .or_else(|| flash_bits_tag.raw_value.as_f64().map(|v| v as u64))
1571            .unwrap_or(0);
1572        if fb_raw != 0 {
1573            let flash_type = if (fb_raw & (1 << 14)) != 0 {
1574                "External"
1575            } else {
1576                "Built-In Flash"
1577            };
1578            result.push(mk_composite(
1579                "FlashType",
1580                "Flash Type",
1581                Value::String(flash_type.to_string()),
1582            ));
1583        }
1584    }
1585
1586    // RedEyeReduction composite:
1587    // Perl: Require CanonFlashMode + FlashBits; suppress if FlashBits==0
1588    //        ValueConv: (CanonFlashMode==3 or ==4 or ==6) ? 1 : 0
1589    if let Some(flash_bits_tag) = find_tag(tags, "FlashBits") {
1590        let fb_raw = flash_bits_tag
1591            .raw_value
1592            .as_u64()
1593            .or_else(|| flash_bits_tag.raw_value.as_f64().map(|v| v as u64))
1594            .unwrap_or(0);
1595        if fb_raw != 0 {
1596            if let Some(cfm_tag) = find_tag(tags, "CanonFlashMode") {
1597                let cfm_raw = cfm_tag
1598                    .raw_value
1599                    .as_u64()
1600                    .or_else(|| cfm_tag.print_value.parse::<u64>().ok())
1601                    .unwrap_or(99);
1602                let red_eye = if cfm_raw == 3 || cfm_raw == 4 || cfm_raw == 6 {
1603                    "On"
1604                } else {
1605                    "Off"
1606                };
1607                result.push(mk_composite(
1608                    "RedEyeReduction",
1609                    "Red Eye Reduction",
1610                    Value::String(red_eye.to_string()),
1611                ));
1612            }
1613        }
1614    }
1615
1616    // ConditionalFEC composite (Flash Exposure Compensation, only when flash fired):
1617    // Perl: Require FlashExposureComp + FlashBits; suppress if FlashBits==0
1618    //        ValueConv: FlashExposureComp; PrintConv: same as FlashExposureComp PrintConv
1619    if let Some(flash_bits_tag) = find_tag(tags, "FlashBits") {
1620        let fb_raw = flash_bits_tag
1621            .raw_value
1622            .as_u64()
1623            .or_else(|| flash_bits_tag.raw_value.as_f64().map(|v| v as u64))
1624            .unwrap_or(0);
1625        if fb_raw != 0 {
1626            if let Some(fec_tag) = find_tag(tags, "FlashExposureComp") {
1627                result.push(mk_composite(
1628                    "ConditionalFEC",
1629                    "Flash Exposure Compensation",
1630                    Value::String(fec_tag.print_value.clone()),
1631                ));
1632            }
1633        }
1634    }
1635
1636    // ShutterCurtainHack composite:
1637    // Perl: Desire ShutterCurtainSync + Require FlashBits; suppress if FlashBits==0
1638    //        ValueConv: defined(ShutterCurtainSync) ? ShutterCurtainSync : 0
1639    //        PrintConv: 0 => '1st-curtain sync', 1 => '2nd-curtain sync'
1640    if let Some(flash_bits_tag) = find_tag(tags, "FlashBits") {
1641        let fb_raw = flash_bits_tag
1642            .raw_value
1643            .as_u64()
1644            .or_else(|| flash_bits_tag.raw_value.as_f64().map(|v| v as u64))
1645            .unwrap_or(0);
1646        if fb_raw != 0 {
1647            let scs = find_tag(tags, "ShutterCurtainSync")
1648                .and_then(|t| t.raw_value.as_u64())
1649                .unwrap_or(0);
1650            let pv = if scs == 0 {
1651                "1st-curtain sync"
1652            } else {
1653                "2nd-curtain sync"
1654            };
1655            result.push(mk_composite(
1656                "ShutterCurtainHack",
1657                "Shutter Curtain Sync",
1658                Value::String(pv.to_string()),
1659            ));
1660        }
1661    }
1662
1663    // WB_RGGBLevels composite (Canon):
1664    // Perl: Require Canon:WhiteBalance; Desire WB_RGGBLevelsAsShot + many WB_ sets
1665    // ValueConv: '$val[1] ? $val[1] : $val[($val[0] || 0) + 2]'
1666    // This means: use WB_RGGBLevelsAsShot if present, else use the WB set for WhiteBalance+2
1667    if find_tag(tags, "WB_RGGBLevels").is_none() {
1668        if let Some(wb_tag) = find_tag(tags, "WhiteBalance") {
1669            let wb_val = wb_tag.raw_value.as_u64().unwrap_or(0);
1670            // Try WB_RGGBLevelsAsShot first
1671            let wb_str = if let Some(asshot) = find_tag(tags, "WB_RGGBLevelsAsShot") {
1672                Some(asshot.print_value.clone())
1673            } else {
1674                // Fall back to the set corresponding to WhiteBalance value
1675                // Perl: index maps: 0=Auto, 1=Daylight, 2=Cloudy, 3=Tungsten,
1676                //   4=Fluorescent, 5=Flash, 6=Custom, 8=Shade, 9=Kelvin
1677                let wb_tag_name = match wb_val {
1678                    0 => "WB_RGGBLevelsAuto",
1679                    1 => "WB_RGGBLevelsDaylight",
1680                    2 => "WB_RGGBLevelsCloudy",
1681                    3 => "WB_RGGBLevelsTungsten",
1682                    4 => "WB_RGGBLevelsFluorescent",
1683                    5 => "WB_RGGBLevelsFlash",
1684                    6 => "WB_RGGBLevelsCustom",
1685                    8 => "WB_RGGBLevelsShade",
1686                    9 => "WB_RGGBLevelsKelvin",
1687                    _ => "WB_RGGBLevelsAuto",
1688                };
1689                find_tag(tags, wb_tag_name).map(|t| t.print_value.clone())
1690            };
1691            if let Some(wb_levels) = wb_str {
1692                if !wb_levels.is_empty() {
1693                    result.push(mk_composite(
1694                        "WB_RGGBLevels",
1695                        "WB RGGB Levels",
1696                        Value::String(wb_levels),
1697                    ));
1698                }
1699            }
1700        }
1701    }
1702
1703    if result.is_empty() {
1704        None
1705    } else {
1706        Some(result)
1707    }
1708}
1709
1710/// Compute white balance RGB ratios.
1711fn compute_wb_balance(tags: &[Tag]) -> Option<Vec<Tag>> {
1712    // Look for WB_RGGBLevels in Canon MakerNotes (raw array)
1713    // Or compute from XResolution ratio
1714    let mut result = Vec::new();
1715
1716    // Try to find WhiteBalance RGGB values from Canon tags
1717    // These would come from Canon ColorData (tag 0x4001) which we decode separately
1718    // For now, check if we have the data from MakerNotes
1719    if let Some(wb) = find_tag(tags, "WB_RGGBLevels")
1720        .or_else(|| find_tag(tags, "WB_RGBGLevels"))
1721        .or_else(|| find_tag(tags, "WB_RBLevels"))
1722    {
1723        // Parse WB levels from either List or space-separated String
1724        let parts: Vec<f64> = match &wb.raw_value {
1725            Value::List(items) => items.iter().filter_map(|v| v.as_f64()).collect(),
1726            Value::String(s) => s
1727                .split_whitespace()
1728                .filter_map(|p| p.parse().ok())
1729                .collect(),
1730            _ => Vec::new(),
1731        };
1732        if parts.len() >= 4 {
1733            // Perl Exif::RedBlueBalance + @rggbLookup: indices for R, G, G, B.
1734            // RGGB=[0,1,2,3], RGBG=[0,1,3,2]. green = (L[g1]+L[g2])/2;
1735            // RedBalance = L[ri]/green, BlueBalance = L[bi]/green.
1736            let (ri, g1i, g2i, bi) = if wb.name.contains("RGBG") {
1737                (0, 1, 3, 2)
1738            } else {
1739                (0, 1, 2, 3)
1740            };
1741            let green = (parts[g1i] + parts[g2i]) / 2.0;
1742            if green > 0.0 {
1743                // PrintConv: int($val * 1e6 + 0.5) * 1e-6, then Perl %s = %.15g
1744                let red_bal = (parts[ri] / green * 1e6 + 0.5).floor() * 1e-6;
1745                let blue_bal = (parts[bi] / green * 1e6 + 0.5).floor() * 1e-6;
1746                result.push(mk_composite(
1747                    "RedBalance",
1748                    "Red Balance",
1749                    Value::String(crate::value::format_g15(red_bal)),
1750                ));
1751                result.push(mk_composite(
1752                    "BlueBalance",
1753                    "Blue Balance",
1754                    Value::String(crate::value::format_g15(blue_bal)),
1755                ));
1756            }
1757        } else if parts.len() == 2 {
1758            // WB_RBLevels (Olympus): green level is 256 (rggbLookup type 8).
1759            // PrintConv: int($val * 1e6 + 0.5) * 1e-6 (round half-up, 6 decimals).
1760            let (r, b) = (parts[0], parts[1]);
1761            let red_bal = (r / 256.0 * 1e6 + 0.5).floor() * 1e-6;
1762            let blue_bal = (b / 256.0 * 1e6 + 0.5).floor() * 1e-6;
1763            result.push(mk_composite(
1764                "RedBalance",
1765                "Red Balance",
1766                Value::String(crate::value::format_g15(red_bal)),
1767            ));
1768            result.push(mk_composite(
1769                "BlueBalance",
1770                "Blue Balance",
1771                Value::String(crate::value::format_g15(blue_bal)),
1772            ));
1773        }
1774    } else if let Some(wb) = find_tag(tags, "WB_GRGBLevels") {
1775        // Fujifilm GRGB format: G, R, G, B
1776        // RedBalance = R / avg(G1, G2); BlueBalance = B / avg(G1, G2)
1777        let parts: Vec<f64> = match &wb.raw_value {
1778            Value::List(items) => items.iter().filter_map(|v| v.as_f64()).collect(),
1779            Value::String(s) => s
1780                .split_whitespace()
1781                .filter_map(|p| p.parse().ok())
1782                .collect(),
1783            _ => Vec::new(),
1784        };
1785        if parts.len() >= 4 {
1786            let g1 = parts[0]; // G
1787            let r = parts[1]; // R
1788            let g2 = parts[2]; // G
1789            let b = parts[3]; // B
1790            let g_avg = (g1 + g2) / 2.0;
1791            if g_avg > 0.0 {
1792                // PrintConv: int($val * 1e6 + 0.5) * 1e-6, then Perl %s = %.15g format
1793                let red_bal = (r / g_avg * 1e6 + 0.5) as i64 as f64 * 1e-6;
1794                let blue_bal = (b / g_avg * 1e6 + 0.5) as i64 as f64 * 1e-6;
1795                let red_print = crate::value::format_g15(red_bal);
1796                let blue_print = crate::value::format_g15(blue_bal);
1797                result.push(mk_composite(
1798                    "RedBalance",
1799                    "Red Balance",
1800                    Value::String(red_print),
1801                ));
1802                result.push(mk_composite(
1803                    "BlueBalance",
1804                    "Blue Balance",
1805                    Value::String(blue_print),
1806                ));
1807            }
1808        }
1809    }
1810
1811    // Panasonic: WBRedLevel, WBGreenLevel, WBBlueLevel as separate EXIF tags
1812    // Perl: RedBalance = $r/$g, BlueBalance = $b/$g (from Exif.pm Composite::RedBalance)
1813    if result.is_empty() {
1814        let r_tag = find_tag(tags, "WBRedLevel").and_then(|t| t.raw_value.as_f64());
1815        let g_tag = find_tag(tags, "WBGreenLevel").and_then(|t| t.raw_value.as_f64());
1816        let b_tag = find_tag(tags, "WBBlueLevel").and_then(|t| t.raw_value.as_f64());
1817        if let (Some(r), Some(g), Some(b)) = (r_tag, g_tag, b_tag) {
1818            if g > 0.0 {
1819                // Perl formula: int($val * 1e6 + 0.5) * 1e-6 (from Exif.pm)
1820                let red_bal = (r / g * 1e6 + 0.5) as i64 as f64 * 1e-6;
1821                let blue_bal = (b / g * 1e6 + 0.5) as i64 as f64 * 1e-6;
1822                result.push(mk_composite(
1823                    "RedBalance",
1824                    "Red Balance",
1825                    Value::String(crate::value::format_g15(red_bal)),
1826                ));
1827                result.push(mk_composite(
1828                    "BlueBalance",
1829                    "Blue Balance",
1830                    Value::String(crate::value::format_g15(blue_bal)),
1831                ));
1832            }
1833        }
1834    }
1835
1836    if result.is_empty() {
1837        None
1838    } else {
1839        Some(result)
1840    }
1841}
1842
1843/// Panasonic AdvancedSceneMode composite.
1844/// Perl: Require => Model + SceneMode + AdvancedSceneType
1845/// Key = "SceneMode AdvancedSceneType" (raw integer values), optionally prefixed by model.
1846fn compute_panasonic_advanced_scene_mode(tags: &[Tag]) -> Option<Tag> {
1847    // Require all three
1848    let model = find_tag_value(tags, "Model")?;
1849    // Need SceneMode raw value (integer) and AdvancedSceneType raw value (integer)
1850    let scene_mode_raw = find_tag(tags, "SceneMode")
1851        .and_then(|t| t.raw_value.as_u64())
1852        .unwrap_or(0);
1853    let adv_type_raw = find_tag(tags, "AdvancedSceneType")
1854        .and_then(|t| t.raw_value.as_u64())
1855        .unwrap_or(1);
1856
1857    // Check it's a Panasonic camera (Make = Panasonic or Leica)
1858    let make = find_tag_value(tags, "Make").unwrap_or_default();
1859    if !make.contains("Panasonic") && !make.contains("Leica") {
1860        return None;
1861    }
1862
1863    // Perl PrintConv: first try model-specific key, then generic key
1864    let _model_key = format!("{} {} {}", model, scene_mode_raw, adv_type_raw);
1865    let generic_key = format!("{} {}", scene_mode_raw, adv_type_raw);
1866
1867    // Model-specific table (only DMC-TZ40 entries in Perl)
1868    let model_val = if model == "DMC-TZ40" {
1869        match generic_key.as_str() {
1870            "90 1" => Some("Expressive"),
1871            "90 2" => Some("Retro"),
1872            "90 3" => Some("High Key"),
1873            "90 4" => Some("Sepia"),
1874            "90 5" => Some("High Dynamic"),
1875            "90 6" => Some("Miniature"),
1876            "90 9" => Some("Low Key"),
1877            "90 10" => Some("Toy Effect"),
1878            "90 11" => Some("Dynamic Monochrome"),
1879            "90 12" => Some("Soft"),
1880            _ => None,
1881        }
1882    } else {
1883        None
1884    };
1885
1886    let print_val = if let Some(v) = model_val {
1887        v.to_string()
1888    } else {
1889        // Generic table lookup
1890        match generic_key.as_str() {
1891            "0 1" => "Off".to_string(),
1892            "2 2" => "Outdoor Portrait".to_string(),
1893            "2 3" => "Indoor Portrait".to_string(),
1894            "2 4" => "Creative Portrait".to_string(),
1895            "3 2" => "Nature".to_string(),
1896            "3 3" => "Architecture".to_string(),
1897            "3 4" => "Creative Scenery".to_string(),
1898            "4 2" => "Outdoor Sports".to_string(),
1899            "4 3" => "Indoor Sports".to_string(),
1900            "4 4" => "Creative Sports".to_string(),
1901            "9 2" => "Flower".to_string(),
1902            "9 3" => "Objects".to_string(),
1903            "9 4" => "Creative Macro".to_string(),
1904            "18 1" => "High Sensitivity".to_string(),
1905            "20 1" => "Fireworks".to_string(),
1906            "21 2" => "Illuminations".to_string(),
1907            "21 4" => "Creative Night Scenery".to_string(),
1908            "26 1" => "High-speed Burst (shot 1)".to_string(),
1909            "27 1" => "High-speed Burst (shot 2)".to_string(),
1910            "29 1" => "Snow".to_string(),
1911            "30 1" => "Starry Sky".to_string(),
1912            "31 1" => "Beach".to_string(),
1913            "36 1" => "High-speed Burst (shot 3)".to_string(),
1914            "39 1" => "Aerial Photo / Underwater / Multi-aspect".to_string(),
1915            "45 2" => "Cinema".to_string(),
1916            "45 7" => "Expressive".to_string(),
1917            "45 8" => "Retro".to_string(),
1918            "45 9" => "Pure".to_string(),
1919            "45 10" => "Elegant".to_string(),
1920            "45 12" => "Monochrome".to_string(),
1921            "45 13" => "Dynamic Art".to_string(),
1922            "45 14" => "Silhouette".to_string(),
1923            "51 2" => "HDR Art".to_string(),
1924            "51 3" => "HDR B&W".to_string(),
1925            "59 1" => "Expressive".to_string(),
1926            "59 2" => "Retro".to_string(),
1927            "59 3" => "High Key".to_string(),
1928            "59 4" => "Sepia".to_string(),
1929            "59 5" => "High Dynamic".to_string(),
1930            "59 6" => "Miniature".to_string(),
1931            "59 9" => "Low Key".to_string(),
1932            "59 10" => "Toy Effect".to_string(),
1933            "59 11" => "Dynamic Monochrome".to_string(),
1934            "59 12" => "Soft".to_string(),
1935            "66 1" => "Impressive Art".to_string(),
1936            "66 2" => "Cross Process".to_string(),
1937            "66 3" => "Color Select".to_string(),
1938            "66 4" => "Star".to_string(),
1939            "90 3" => "Old Days".to_string(),
1940            "90 4" => "Sunshine".to_string(),
1941            "90 5" => "Bleach Bypass".to_string(),
1942            "90 6" => "Toy Pop".to_string(),
1943            "90 7" => "Fantasy".to_string(),
1944            "90 8" => "Monochrome".to_string(),
1945            "90 9" => "Rough Monochrome".to_string(),
1946            "90 10" => "Silky Monochrome".to_string(),
1947            "92 1" => "Handheld Night Shot".to_string(),
1948            _ => {
1949                // OTHER handler: lookup shooting mode name, add AdvancedSceneType modifier
1950                // shootingMode table (Panasonic.pm %shootingMode)
1951                let shooting_mode_name = panasonic_shooting_mode(scene_mode_raw);
1952                if let Some(name) = shooting_mode_name {
1953                    match adv_type_raw {
1954                        1 => name.to_string(),
1955                        5 => format!("{} (intelligent auto)", name),
1956                        7 => format!("{} (intelligent auto plus)", name),
1957                        n => format!("{} ({})", name, n),
1958                    }
1959                } else {
1960                    format!("Unknown ({} {})", scene_mode_raw, adv_type_raw)
1961                }
1962            }
1963        }
1964    };
1965
1966    // Raw value: "Model SceneMode AdvancedSceneType" (Perl ValueConv)
1967    let raw_str = format!("{} {} {}", model, scene_mode_raw, adv_type_raw);
1968    Some(mk_composite_raw(
1969        "AdvancedSceneMode",
1970        "Advanced Scene Mode",
1971        Value::String(raw_str),
1972        print_val,
1973    ))
1974}
1975
1976/// Panasonic ShootingMode/SceneMode name lookup (from %shootingMode in Panasonic.pm)
1977fn panasonic_shooting_mode(val: u64) -> Option<&'static str> {
1978    match val {
1979        1 => Some("Normal"),
1980        2 => Some("Portrait"),
1981        3 => Some("Scenery"),
1982        4 => Some("Sports"),
1983        5 => Some("Night Portrait"),
1984        6 => Some("Program"),
1985        7 => Some("Aperture Priority"),
1986        8 => Some("Shutter Priority"),
1987        9 => Some("Macro"),
1988        10 => Some("Spot"),
1989        11 => Some("Manual"),
1990        12 => Some("Movie Preview"),
1991        13 => Some("Panning"),
1992        14 => Some("Simple"),
1993        15 => Some("Color Effects"),
1994        16 => Some("Self Portrait"),
1995        17 => Some("Economy"),
1996        18 => Some("Fireworks"),
1997        19 => Some("Party"),
1998        20 => Some("Snow"),
1999        21 => Some("Night Scenery"),
2000        22 => Some("Food"),
2001        23 => Some("Baby"),
2002        24 => Some("Soft Skin"),
2003        25 => Some("Candlelight"),
2004        26 => Some("Starry Night"),
2005        27 => Some("High Sensitivity"),
2006        28 => Some("Panorama Assist"),
2007        29 => Some("Underwater"),
2008        30 => Some("Beach"),
2009        31 => Some("Aerial Photo"),
2010        32 => Some("Sunset"),
2011        33 => Some("Pet"),
2012        34 => Some("Intelligent ISO"),
2013        35 => Some("Clipboard"),
2014        36 => Some("High Speed Continuous Shooting"),
2015        37 => Some("Intelligent Auto"),
2016        39 => Some("Multi-aspect"),
2017        41 => Some("Transform"),
2018        42 => Some("Flash Burst"),
2019        43 => Some("Pin Hole"),
2020        44 => Some("Film Grain"),
2021        45 => Some("My Color"),
2022        46 => Some("Photo Frame"),
2023        48 => Some("Movie"),
2024        51 => Some("HDR"),
2025        52 => Some("Peripheral Defocus"),
2026        55 => Some("Handheld Night Shot"),
2027        57 => Some("3D"),
2028        59 => Some("Creative Control"),
2029        60 => Some("Intelligent Auto Plus"),
2030        62 => Some("Panorama"),
2031        63 => Some("Glass Through"),
2032        64 => Some("HDR"),
2033        66 => Some("Digital Filter"),
2034        67 => Some("Clear Portrait"),
2035        68 => Some("Silky Skin"),
2036        69 => Some("Backlit Softness"),
2037        70 => Some("Clear in Backlight"),
2038        71 => Some("Relaxing Tone"),
2039        72 => Some("Sweet Child's Face"),
2040        73 => Some("Distinct Scenery"),
2041        74 => Some("Bright Blue Sky"),
2042        75 => Some("Romantic Sunset Glow"),
2043        76 => Some("Vivid Sunset Glow"),
2044        77 => Some("Glistening Water"),
2045        78 => Some("Clear Nightscape"),
2046        79 => Some("Cool Night Sky"),
2047        _ => None,
2048    }
2049}
2050
2051/// Compute Depth of Field.
2052/// Compute DOF using exact Perl ExifTool formula from Exif.pm line 4775.
2053/// Require: FocalLength, Aperture (=FNumber), CircleOfConfusion
2054/// Desire: FocusDistance, SubjectDistance, FocusDistanceLower/Upper
2055fn compute_dof(tags: &[Tag]) -> Option<Vec<Tag>> {
2056    let f = find_tag_f64(tags, "FocalLength")?; // mm
2057    let aperture = find_tag_f64(tags, "FNumber")?;
2058    let coc = find_tag_f64(tags, "CircleOfConfusion").or_else(|| {
2059        find_tag_value(tags, "CircleOfConfusion")
2060            .and_then(|s| s.split_whitespace().next()?.parse::<f64>().ok())
2061    })?;
2062
2063    if f <= 0.0 || coc <= 0.0 {
2064        return None;
2065    }
2066
2067    // Find focus distance (meters). Try multiple sources like Perl does.
2068    let d = find_tag_f64(tags, "FocusDistance")
2069        .or_else(|| {
2070            find_tag_value(tags, "FocusDistance")
2071                .and_then(|s| s.split_whitespace().next()?.parse::<f64>().ok())
2072                .filter(|v| v.is_finite())
2073        })
2074        // Perl: $val[4] || $val[5] || $val[6] — 0 means "not available" for these
2075        .or_else(|| find_tag_f64(tags, "SubjectDistance").filter(|&v| v > 0.0))
2076        .or_else(|| {
2077            // Prefer the printed value ("1 m") — it reflects ValueConv ($val/1000),
2078            // whereas the raw stays in mm for some makers (Casio).
2079            find_tag_value(tags, "ObjectDistance")
2080                .and_then(|s| s.split_whitespace().next()?.parse().ok())
2081                .filter(|&v: &f64| v > 0.0)
2082                .or_else(|| find_tag_f64(tags, "ObjectDistance").filter(|&v| v > 0.0))
2083        })
2084        .or_else(|| {
2085            find_tag_f64(tags, "ApproximateFocusDistance")
2086                .filter(|&v| v > 0.0)
2087                .or_else(|| {
2088                    find_tag_value(tags, "ApproximateFocusDistance")
2089                        .and_then(|s| s.split_whitespace().next()?.parse().ok())
2090                        .filter(|&v: &f64| v > 0.0)
2091                })
2092        })
2093        .or_else(|| {
2094            let upper = find_tag_f64(tags, "FocusDistanceUpper").or_else(|| {
2095                find_tag_value(tags, "FocusDistanceUpper")
2096                    .and_then(|s| s.split_whitespace().next()?.parse().ok())
2097            });
2098            let lower = find_tag_f64(tags, "FocusDistanceLower").or_else(|| {
2099                find_tag_value(tags, "FocusDistanceLower")
2100                    .and_then(|s| s.split_whitespace().next()?.parse().ok())
2101            });
2102            match (upper, lower) {
2103                (Some(u), Some(l)) => Some((u + l) / 2.0),
2104                _ => None,
2105            }
2106        });
2107
2108    // Require focus distance (return None if missing)
2109    let d = d?;
2110    let d = if d == 0.0 { 1e10 } else { d }; // 0 = infinity
2111
2112    // Perl formula: t = aperture * coc * (d*1000 - f) / (f * f)
2113    let t = aperture * coc * (d * 1000.0 - f) / (f * f);
2114    let near = d / (1.0 + t);
2115    let mut far = d / (1.0 - t);
2116    if far < 0.0 {
2117        far = 0.0;
2118    } // 0 means infinity
2119
2120    let dof_str = if far == 0.0 {
2121        format!("inf ({:.2} m - inf)", near)
2122    } else {
2123        let dof = far - near;
2124        if dof > 0.0 && dof < 0.02 {
2125            format!("{:.3} m ({:.3} - {:.3} m)", dof, near, far)
2126        } else {
2127            format!("{:.2} m ({:.2} - {:.2} m)", dof, near, far)
2128        }
2129    };
2130
2131    Some(vec![mk_composite(
2132        "DOF",
2133        "Depth of Field",
2134        Value::String(dof_str),
2135    )])
2136}
2137
2138/// Reverse geocode GPS position using Geolocation.dat.
2139/// Reverse-geocode `Geolocation*` tags from GPS coordinates.
2140///
2141/// ExifTool only emits these with the `Geolocation` API option enabled, so the
2142/// caller gates this on `Options::geolocation` (off by default).
2143pub fn compute_geolocation(tags: &[Tag]) -> Option<Vec<Tag>> {
2144    use crate::geolocation::GeolocationDb;
2145    use std::sync::OnceLock;
2146
2147    // Parse GPS coordinates
2148    let lat_tag = find_tag(tags, "GPSLatitude")?;
2149    let lon_tag = find_tag(tags, "GPSLongitude")?;
2150    let lat_ref = find_tag_value(tags, "GPSLatitudeRef").unwrap_or_default();
2151    let lon_ref = find_tag_value(tags, "GPSLongitudeRef").unwrap_or_default();
2152
2153    let lat = parse_gps_decimal(&lat_tag.raw_value, &lat_ref)?;
2154    let lon = parse_gps_decimal(&lon_tag.raw_value, &lon_ref)?;
2155
2156    // Load database (cached via OnceLock)
2157    static DB: OnceLock<Option<GeolocationDb>> = OnceLock::new();
2158    let db = DB.get_or_init(GeolocationDb::load_default);
2159
2160    let db = db.as_ref()?;
2161    let city = db.find_nearest(lat, lon)?;
2162
2163    let mut geo_tags = Vec::new();
2164    geo_tags.push(mk_composite(
2165        "GeolocationCity",
2166        "Geolocation City",
2167        Value::String(city.name.clone()),
2168    ));
2169    geo_tags.push(mk_composite(
2170        "GeolocationCountryCode",
2171        "Geolocation Country Code",
2172        Value::String(city.country_code.clone()),
2173    ));
2174    geo_tags.push(mk_composite(
2175        "GeolocationCountry",
2176        "Geolocation Country",
2177        Value::String(city.country.clone()),
2178    ));
2179    if !city.region.is_empty() {
2180        geo_tags.push(mk_composite(
2181            "GeolocationRegion",
2182            "Geolocation Region",
2183            Value::String(city.region.clone()),
2184        ));
2185    }
2186    if !city.subregion.is_empty() {
2187        geo_tags.push(mk_composite(
2188            "GeolocationSubregion",
2189            "Geolocation Subregion",
2190            Value::String(city.subregion.clone()),
2191        ));
2192    }
2193    if !city.timezone.is_empty() {
2194        geo_tags.push(mk_composite(
2195            "GeolocationTimeZone",
2196            "Geolocation Time Zone",
2197            Value::String(city.timezone.clone()),
2198        ));
2199    }
2200
2201    Some(geo_tags)
2202}
2203
2204fn parse_gps_decimal(value: &Value, reference: &str) -> Option<f64> {
2205    let decimal = match value {
2206        Value::List(items) if items.len() >= 3 => {
2207            let deg = items[0].as_f64()?;
2208            let min = items[1].as_f64()?;
2209            let sec = items[2].as_f64()?;
2210            deg + min / 60.0 + sec / 3600.0
2211        }
2212        Value::URational(n, d) if *d > 0 => *n as f64 / *d as f64,
2213        _ => return None,
2214    };
2215    let sign = if reference == "S" || reference == "W" {
2216        -1.0
2217    } else {
2218        1.0
2219    };
2220    Some(decimal * sign)
2221}
2222
2223/// Compute Hyperfocal Distance: H = f² / (N × c) + f
2224/// Requires CircleOfConfusion from composites (not hardcoded).
2225fn compute_hyperfocal(tags: &[Tag]) -> Option<Tag> {
2226    let fl = find_tag_f64(tags, "FocalLength")?;
2227    let fnum = find_tag_f64(tags, "FNumber")?;
2228
2229    if fl <= 0.0 || fnum <= 0.0 {
2230        return None;
2231    }
2232
2233    // Get CircleOfConfusion from composites
2234    let coc = find_tag_f64(tags, "CircleOfConfusion").or_else(|| {
2235        find_tag_value(tags, "CircleOfConfusion")
2236            .and_then(|s| s.split_whitespace().next()?.parse::<f64>().ok())
2237    })?;
2238
2239    if coc <= 0.0 {
2240        return None;
2241    }
2242
2243    // Perl formula: $val[0]^2 / ($val[1] * $val[2] * 1000)
2244    // where val[0]=FocalLength(mm), val[1]=Aperture(f-number), val[2]=CoC(mm)
2245    // The /1000 converts from mm to m (result directly in m, no need to divide again)
2246    let h_m = (fl * fl) / (fnum * coc * 1000.0);
2247
2248    Some(mk_composite(
2249        "HyperfocalDistance",
2250        "Hyperfocal Distance",
2251        Value::String(format!("{:.2} m", h_m)),
2252    ))
2253}
2254
2255/// QuickTime Composite `Rotation` (QuickTime.pm:8632): Require MatrixStructure
2256/// and HandlerType, ValueConv `CalcRotation` (QuickTime.pm:8797). CalcRotation
2257/// walks the HandlerType entries for the first one whose value is 'vide', notes
2258/// its family-1 group, then takes the MatrixStructure of that same group and
2259/// runs it through GetRotationAngle. One value per file, in the Composite group
2260/// — not one per track.
2261fn compute_quicktime_rotation(tags: &[Tag]) -> Option<Tag> {
2262    let track = tags
2263        .iter()
2264        .find(|t| {
2265            t.name == "HandlerType"
2266                && t.group.family0 == "QuickTime"
2267                && t.print_value == "Video Track"
2268        })?
2269        .group
2270        .family1
2271        .clone();
2272    let matrix = tags.iter().find(|t| {
2273        t.name == "MatrixStructure" && t.group.family0 == "QuickTime" && t.group.family1 == track
2274    })?;
2275    let angle = crate::formats::quicktime::rotation_angle_from_matrix(&matrix.print_value)?;
2276    // `{}` renders 90.0 as "90" and 90.001 as "90.001", matching Perl.
2277    Some(mk_composite(
2278        "Rotation",
2279        "Rotation",
2280        Value::String(format!("{}", angle)),
2281    ))
2282}
2283
2284/// Olympus Composite `ExtenderStatus` (Olympus.pm:4283-4300).
2285///
2286/// `Require => { 0 => 'Olympus:Extender', 1 => 'Olympus:LensType',
2287/// 2 => 'MaxApertureValue' }` — all three, so the tag exists only when all
2288/// three do — with `ValueConv => Image::ExifTool::Olympus::ExtenderStatus(
2289/// $val[0], $prt[1], $val[2])` and `PrintConv => { 0 => 'Not attached',
2290/// 1 => 'Attached', 2 => 'Removed' }`. The table declares
2291/// `GROUPS => { 2 => 'Camera' }` (Olympus.pm:4282) and AddCompositeTags supplies
2292/// families 0 and 1, so it is reported in the Composite group, not in Olympus.
2293fn compute_olympus_extender_status(tags: &[Tag]) -> Option<Tag> {
2294    let olympus = |name: &str| -> Option<&Tag> {
2295        tags.iter()
2296            .find(|t| t.name == name && t.group.family1 == "Olympus")
2297    };
2298    let extender = olympus("Extender")?;
2299    let lens_type = olympus("LensType")?;
2300    // $val[2] is the ValueConv f-number. We read the printed f-number, which is
2301    // that value rounded for display; it only matters for the 0.2 threshold of
2302    // the EC-14 branch below.
2303    let max_aperture: f64 = find_tag(tags, "MaxApertureValue")?
2304        .print_value
2305        .parse()
2306        .ok()?;
2307
2308    // sub ExtenderStatus (Olympus.pm:4337-4352).
2309    let key = crate::tags::makernotes::olympus_extender_key(&extender.print_value);
2310    let info: Vec<&str> = key.split_whitespace().collect();
2311    // "validate that extender identifier is reasonable"
2312    //     return 0 unless @info >= 2 and hex($info[1]);
2313    let status = if info.len() < 2 || u32::from_str_radix(info[1], 16).unwrap_or(0) == 0 {
2314        0u32
2315    } else if format!("{} {}", info[0], info[1]) != "0 04" {
2316        // "if it's not an EC-14 (id '0 04') then assume it was really attached"
2317        1
2318    } else {
2319        // "$lensType =~ / F(\d+(\.\d+)?)/ or return 1" — the lens's own maximum
2320        // aperture, then "return(($maxAperture - $1 > 0.2) ? 1 : 2)".
2321        match lens_max_aperture(&lens_type.print_value) {
2322            None => 1,
2323            Some(f) => {
2324                if max_aperture - f > 0.2 {
2325                    1
2326                } else {
2327                    2
2328                }
2329            }
2330        }
2331    };
2332
2333    let print = match status {
2334        0 => "Not attached",
2335        1 => "Attached",
2336        _ => "Removed",
2337    };
2338    let mut t = mk_composite_raw(
2339        "ExtenderStatus",
2340        "Extender Status",
2341        Value::U32(status),
2342        print.to_string(),
2343    );
2344    t.group.family2 = "Camera".into();
2345    Some(t)
2346}
2347
2348/// `$lensType =~ / F(\d+(\.\d+)?)/` (Olympus.pm:4348).
2349fn lens_max_aperture(lens_type: &str) -> Option<f64> {
2350    let rest = lens_type.split(" F").nth(1)?;
2351    let end = rest
2352        .find(|c: char| !c.is_ascii_digit() && c != '.')
2353        .unwrap_or(rest.len());
2354    rest[..end].parse().ok()
2355}
2356
2357/// XMP Composite `Flash` (XMP.pm:2808-2840).
2358///
2359/// `Desire => { 0 => 'XMP:FlashFired', 1 => 'XMP:FlashReturn', 2 => 'XMP:FlashMode',
2360/// 3 => 'XMP:FlashFunction', 4 => 'XMP:FlashRedEyeMode', 5 => 'XMP:Flash' }`, all
2361/// Desire, so the tag is built as soon as any one of them exists. The ValueConv
2362/// packs them into the EXIF Flash bitmask and the PrintConv is
2363/// `%Image::ExifTool::Exif::flash`.
2364///
2365/// It has no `Priority` and no `Inhibit`, so it competes with an EXIF `Flash` on
2366/// equal terms — and wins, because ExifTool builds Composites after the whole
2367/// file has been read (BuildCompositeTags) and equal priorities are last-wins.
2368/// That is why this lives here and not in the XMP reader.
2369fn compute_xmp_flash(tags: &[Tag]) -> Option<Tag> {
2370    let xmp = |name: &str| -> Option<&Tag> {
2371        tags.iter()
2372            .find(|t| t.name == name && t.group.family0 == "XMP")
2373    };
2374    // The ValueConv reads the raw XMP property text ('True'/'False', '0'..'3'),
2375    // not the printed form the XMP reader later derives from the shared EXIF
2376    // PrintConv ('Off', 'No return detection', ...).
2377    let get_bool = |name: &str| -> Option<bool> {
2378        xmp(name).map(|t| t.raw_value.to_display_string().eq_ignore_ascii_case("true"))
2379    };
2380    let get_int = |name: &str| -> Option<u32> {
2381        xmp(name).and_then(|t| t.raw_value.to_display_string().parse::<u32>().ok())
2382    };
2383
2384    let fired = get_bool("FlashFired");
2385    let ret = get_int("FlashReturn");
2386    let mode = get_int("FlashMode");
2387    let function = get_bool("FlashFunction");
2388    let red_eye = get_bool("FlashRedEyeMode");
2389    if fired.is_none() && ret.is_none() && mode.is_none() && function.is_none() && red_eye.is_none()
2390    {
2391        return None;
2392    }
2393
2394    let val: u32 = u32::from(fired.unwrap_or(false))
2395        | ((ret.unwrap_or(0) & 0x03) << 1)
2396        | ((mode.unwrap_or(0) & 0x03) << 3)
2397        | (if function.unwrap_or(false) { 0x20 } else { 0 })
2398        | (if red_eye.unwrap_or(false) { 0x40 } else { 0 });
2399
2400    let mut t = mk_composite_raw(
2401        "Flash",
2402        "Flash",
2403        Value::String(val.to_string()),
2404        crate::metadata::xmp::flash_numeric_to_string(val),
2405    );
2406    // The entry's own `Groups => { 2 => 'Camera' }` (XMP.pm:2820).
2407    t.group.family2 = "Camera".into();
2408    Some(t)
2409}
2410
2411fn mk_composite_raw(name: &str, description: &str, value: Value, print_value: String) -> Tag {
2412    Tag {
2413        id: TagId::Text(name.to_string()),
2414        name: name.to_string(),
2415        description: description.to_string(),
2416        group: TagGroup {
2417            family0: "Composite".to_string(),
2418            family1: "Composite".to_string(),
2419            family2: "Other".to_string(),
2420            family3: "Main".into(),
2421        },
2422        raw_value: value,
2423        print_value,
2424        priority: 0,
2425    }
2426}
2427
2428fn mk_composite(name: &str, description: &str, value: Value) -> Tag {
2429    let pv = value.to_display_string();
2430    Tag {
2431        id: TagId::Text(name.to_string()),
2432        name: name.to_string(),
2433        description: description.to_string(),
2434        group: TagGroup {
2435            family0: "Composite".to_string(),
2436            family1: "Composite".to_string(),
2437            family2: "Other".to_string(),
2438            family3: "Main".into(),
2439        },
2440        raw_value: value,
2441        print_value: pv,
2442        priority: 0,
2443    }
2444}
2445
2446/// Compute Nikon LensID from lens data tags.
2447/// Perl: ValueConv => 'sprintf("%.2X"." %.2X"x7, @raw)', PrintConv => \%nikonLensIDs
2448/// The 8 raw bytes are: LensIDNumber, LensFStops, MinFocalLength, MaxFocalLength,
2449/// MaxApertureAtMinFocal, MaxApertureAtMaxFocal, MCUVersion, LensType
2450fn compute_nikon_lens_id(tags: &[Tag]) -> Option<String> {
2451    // Byte 0: LensIDNumber (raw integer value)
2452    let lens_id_num = find_tag(tags, "LensIDNumber").and_then(|t| {
2453        t.raw_value
2454            .as_u64()
2455            .or_else(|| t.print_value.trim().parse::<u64>().ok())
2456    })? as u8;
2457
2458    // Byte 1: LensFStops raw byte
2459    // From main Nikon tag 0x008B (undef[4]): bytes a,b,c,d → val = a*(b/c)
2460    // The raw byte for LensID key = byte 0 of the Undefined value (= a)
2461    let lens_fstops_byte = find_tag(tags, "LensFStops")
2462        .and_then(|t| {
2463            match &t.raw_value {
2464                Value::Undefined(bytes) | Value::Binary(bytes) if !bytes.is_empty() => {
2465                    Some(bytes[0])
2466                }
2467                _ => {
2468                    // Fall back: reverse from print value (val * 12)
2469                    t.print_value
2470                        .trim()
2471                        .parse::<f64>()
2472                        .ok()
2473                        .map(|v| (v * 12.0).round() as u8)
2474                }
2475            }
2476        })
2477        .unwrap_or(0);
2478
2479    // Byte 2: MinFocalLength raw byte — reverse: 24 * log2(val/5)
2480    let min_focal_byte = find_tag(tags, "MinFocalLength")
2481        .and_then(|t| {
2482            t.raw_value
2483                .as_f64()
2484                .or_else(|| t.print_value.split_whitespace().next()?.parse::<f64>().ok())
2485                .filter(|&v| v > 0.0)
2486                .map(|v| (24.0 * (v / 5.0).log2()).round() as u8)
2487        })
2488        .unwrap_or(0);
2489
2490    // Byte 3: MaxFocalLength raw byte — reverse: 24 * log2(val/5)
2491    let max_focal_byte = find_tag(tags, "MaxFocalLength")
2492        .and_then(|t| {
2493            t.raw_value
2494                .as_f64()
2495                .or_else(|| t.print_value.split_whitespace().next()?.parse::<f64>().ok())
2496                .filter(|&v| v > 0.0)
2497                .map(|v| (24.0 * (v / 5.0).log2()).round() as u8)
2498        })
2499        .unwrap_or(0);
2500
2501    // Byte 4: MaxApertureAtMinFocal raw byte — reverse: 24 * log2(val)
2502    let max_apt_min_byte = find_tag(tags, "MaxApertureAtMinFocal")
2503        .and_then(|t| {
2504            t.raw_value
2505                .as_f64()
2506                .or_else(|| t.print_value.trim().parse::<f64>().ok())
2507                .filter(|&v| v > 0.0)
2508                .map(|v| (24.0 * v.log2()).round() as u8)
2509        })
2510        .unwrap_or(0);
2511
2512    // Byte 5: MaxApertureAtMaxFocal raw byte — reverse: 24 * log2(val)
2513    let max_apt_max_byte = find_tag(tags, "MaxApertureAtMaxFocal")
2514        .and_then(|t| {
2515            t.raw_value
2516                .as_f64()
2517                .or_else(|| t.print_value.trim().parse::<f64>().ok())
2518                .filter(|&v| v > 0.0)
2519                .map(|v| (24.0 * v.log2()).round() as u8)
2520        })
2521        .unwrap_or(0);
2522
2523    // Byte 6: MCUVersion (raw integer; may be wrong due to makernotes offset issue)
2524    let mcu_version_byte = find_tag(tags, "MCUVersion")
2525        .and_then(|t| {
2526            t.raw_value
2527                .as_u64()
2528                .or_else(|| t.print_value.trim().parse::<u64>().ok())
2529        })
2530        .unwrap_or(0) as u8;
2531
2532    // Byte 7: LensType (raw integer value, lower byte)
2533    let lens_type_byte = find_tag(tags, "LensType")
2534        .and_then(|t| {
2535            t.raw_value
2536                .as_u64()
2537                .or_else(|| t.print_value.trim().parse::<u64>().ok())
2538        })
2539        .unwrap_or(0) as u8;
2540
2541    let key = [
2542        lens_id_num,
2543        lens_fstops_byte,
2544        min_focal_byte,
2545        max_focal_byte,
2546        max_apt_min_byte,
2547        max_apt_max_byte,
2548        mcu_version_byte,
2549        lens_type_byte,
2550    ];
2551
2552    nikon_lens_id_lookup(&key)
2553}
2554
2555/// Look up Nikon lens name from 8-byte key.
2556/// First tries exact match, then partial match ignoring byte 6 (MCUVersion).
2557fn nikon_lens_id_lookup(key: &[u8; 8]) -> Option<String> {
2558    // Try exact match first
2559    for &(ref k, name) in NIKON_LENS_IDS {
2560        if k == key {
2561            return Some(name.to_string());
2562        }
2563    }
2564    // Partial match: ignore byte 6 (MCUVersion may not be stored correctly)
2565    // Match on bytes 0,1,2,3,4,5,7
2566    let mut matches: Vec<&str> = Vec::new();
2567    for &(ref k, name) in NIKON_LENS_IDS {
2568        if k[0] == key[0]
2569            && k[1] == key[1]
2570            && k[2] == key[2]
2571            && k[3] == key[3]
2572            && k[4] == key[4]
2573            && k[5] == key[5]
2574            && k[7] == key[7]
2575        {
2576            matches.push(name);
2577        }
2578    }
2579    if matches.len() == 1 {
2580        return Some(matches[0].to_string());
2581    }
2582    None
2583}
2584
2585/// Nikon lens ID lookup table.
2586/// Keys are 8 bytes: LensIDNumber, LensFStops, MinFocalLength, MaxFocalLength,
2587/// MaxApertureAtMinFocal, MaxApertureAtMaxFocal, MCUVersion, LensType.
2588/// From Perl ExifTool Nikon.pm %nikonLensIDs.
2589static NIKON_LENS_IDS: &[([u8; 8], &str)] = &[
2590    (
2591        [0x01, 0x58, 0x50, 0x50, 0x14, 0x14, 0x02, 0x00],
2592        "AF Nikkor 50mm f/1.8",
2593    ),
2594    (
2595        [0x01, 0x58, 0x50, 0x50, 0x14, 0x14, 0x05, 0x00],
2596        "AF Nikkor 50mm f/1.8",
2597    ),
2598    (
2599        [0x02, 0x42, 0x44, 0x5C, 0x2A, 0x34, 0x02, 0x00],
2600        "AF Zoom-Nikkor 35-70mm f/3.3-4.5",
2601    ),
2602    (
2603        [0x02, 0x42, 0x44, 0x5C, 0x2A, 0x34, 0x08, 0x00],
2604        "AF Zoom-Nikkor 35-70mm f/3.3-4.5",
2605    ),
2606    (
2607        [0x03, 0x48, 0x5C, 0x81, 0x30, 0x30, 0x02, 0x00],
2608        "AF Zoom-Nikkor 70-210mm f/4",
2609    ),
2610    (
2611        [0x04, 0x48, 0x3C, 0x3C, 0x24, 0x24, 0x03, 0x00],
2612        "AF Nikkor 28mm f/2.8",
2613    ),
2614    (
2615        [0x05, 0x54, 0x50, 0x50, 0x0C, 0x0C, 0x04, 0x00],
2616        "AF Nikkor 50mm f/1.4",
2617    ),
2618    (
2619        [0x06, 0x54, 0x53, 0x53, 0x24, 0x24, 0x06, 0x00],
2620        "AF Micro-Nikkor 55mm f/2.8",
2621    ),
2622    (
2623        [0x07, 0x40, 0x3C, 0x62, 0x2C, 0x34, 0x03, 0x00],
2624        "AF Zoom-Nikkor 28-85mm f/3.5-4.5",
2625    ),
2626    (
2627        [0x08, 0x40, 0x44, 0x6A, 0x2C, 0x34, 0x04, 0x00],
2628        "AF Zoom-Nikkor 35-105mm f/3.5-4.5",
2629    ),
2630    (
2631        [0x09, 0x48, 0x37, 0x37, 0x24, 0x24, 0x04, 0x00],
2632        "AF Nikkor 24mm f/2.8",
2633    ),
2634    (
2635        [0x0A, 0x48, 0x8E, 0x8E, 0x24, 0x24, 0x03, 0x00],
2636        "AF Nikkor 300mm f/2.8 IF-ED",
2637    ),
2638    (
2639        [0x0A, 0x48, 0x8E, 0x8E, 0x24, 0x24, 0x05, 0x00],
2640        "AF Nikkor 300mm f/2.8 IF-ED N",
2641    ),
2642    (
2643        [0x0B, 0x48, 0x7C, 0x7C, 0x24, 0x24, 0x05, 0x00],
2644        "AF Nikkor 180mm f/2.8 IF-ED",
2645    ),
2646    (
2647        [0x0D, 0x40, 0x44, 0x72, 0x2C, 0x34, 0x07, 0x00],
2648        "AF Zoom-Nikkor 35-135mm f/3.5-4.5",
2649    ),
2650    (
2651        [0x0E, 0x48, 0x5C, 0x81, 0x30, 0x30, 0x05, 0x00],
2652        "AF Zoom-Nikkor 70-210mm f/4",
2653    ),
2654    (
2655        [0x0F, 0x58, 0x50, 0x50, 0x14, 0x14, 0x05, 0x00],
2656        "AF Nikkor 50mm f/1.8 N",
2657    ),
2658    (
2659        [0x10, 0x48, 0x8E, 0x8E, 0x30, 0x30, 0x08, 0x00],
2660        "AF Nikkor 300mm f/4 IF-ED",
2661    ),
2662    (
2663        [0x11, 0x48, 0x44, 0x5C, 0x24, 0x24, 0x08, 0x00],
2664        "AF Zoom-Nikkor 35-70mm f/2.8",
2665    ),
2666    (
2667        [0x11, 0x48, 0x44, 0x5C, 0x24, 0x24, 0x15, 0x00],
2668        "AF Zoom-Nikkor 35-70mm f/2.8",
2669    ),
2670    (
2671        [0x12, 0x48, 0x5C, 0x81, 0x30, 0x3C, 0x09, 0x00],
2672        "AF Nikkor 70-210mm f/4-5.6",
2673    ),
2674    (
2675        [0x13, 0x42, 0x37, 0x50, 0x2A, 0x34, 0x0B, 0x00],
2676        "AF Zoom-Nikkor 24-50mm f/3.3-4.5",
2677    ),
2678    (
2679        [0x14, 0x48, 0x60, 0x80, 0x24, 0x24, 0x0B, 0x00],
2680        "AF Zoom-Nikkor 80-200mm f/2.8 ED",
2681    ),
2682    (
2683        [0x15, 0x4C, 0x62, 0x62, 0x14, 0x14, 0x0C, 0x00],
2684        "AF Nikkor 85mm f/1.8",
2685    ),
2686    (
2687        [0x17, 0x3C, 0xA0, 0xA0, 0x30, 0x30, 0x0F, 0x00],
2688        "Nikkor 500mm f/4 P ED IF",
2689    ),
2690    (
2691        [0x17, 0x3C, 0xA0, 0xA0, 0x30, 0x30, 0x11, 0x00],
2692        "Nikkor 500mm f/4 P ED IF",
2693    ),
2694    (
2695        [0x18, 0x40, 0x44, 0x72, 0x2C, 0x34, 0x0E, 0x00],
2696        "AF Zoom-Nikkor 35-135mm f/3.5-4.5 N",
2697    ),
2698    (
2699        [0x1A, 0x54, 0x44, 0x44, 0x18, 0x18, 0x11, 0x00],
2700        "AF Nikkor 35mm f/2",
2701    ),
2702    (
2703        [0x1B, 0x44, 0x5E, 0x8E, 0x34, 0x3C, 0x10, 0x00],
2704        "AF Zoom-Nikkor 75-300mm f/4.5-5.6",
2705    ),
2706    (
2707        [0x1C, 0x48, 0x30, 0x30, 0x24, 0x24, 0x12, 0x00],
2708        "AF Nikkor 20mm f/2.8",
2709    ),
2710    (
2711        [0x1D, 0x42, 0x44, 0x5C, 0x2A, 0x34, 0x12, 0x00],
2712        "AF Zoom-Nikkor 35-70mm f/3.3-4.5 N",
2713    ),
2714    (
2715        [0x1E, 0x54, 0x56, 0x56, 0x24, 0x24, 0x13, 0x00],
2716        "AF Micro-Nikkor 60mm f/2.8",
2717    ),
2718    (
2719        [0x1F, 0x54, 0x6A, 0x6A, 0x24, 0x24, 0x14, 0x00],
2720        "AF Micro-Nikkor 105mm f/2.8",
2721    ),
2722    (
2723        [0x20, 0x48, 0x60, 0x80, 0x24, 0x24, 0x15, 0x00],
2724        "AF Zoom-Nikkor 80-200mm f/2.8 ED",
2725    ),
2726    (
2727        [0x21, 0x40, 0x3C, 0x5C, 0x2C, 0x34, 0x16, 0x00],
2728        "AF Zoom-Nikkor 28-70mm f/3.5-4.5",
2729    ),
2730    (
2731        [0x22, 0x48, 0x72, 0x72, 0x18, 0x18, 0x16, 0x00],
2732        "AF DC-Nikkor 135mm f/2",
2733    ),
2734    (
2735        [0x23, 0x30, 0xBE, 0xCA, 0x3C, 0x48, 0x17, 0x00],
2736        "Zoom-Nikkor 1200-1700mm f/5.6-8 P ED IF",
2737    ),
2738    (
2739        [0x24, 0x48, 0x60, 0x80, 0x24, 0x24, 0x1A, 0x02],
2740        "AF Zoom-Nikkor 80-200mm f/2.8D ED",
2741    ),
2742    (
2743        [0x25, 0x48, 0x44, 0x5C, 0x24, 0x24, 0x1B, 0x02],
2744        "AF Zoom-Nikkor 35-70mm f/2.8D",
2745    ),
2746    (
2747        [0x25, 0x48, 0x44, 0x5C, 0x24, 0x24, 0x3A, 0x02],
2748        "AF Zoom-Nikkor 35-70mm f/2.8D",
2749    ),
2750    (
2751        [0x25, 0x48, 0x44, 0x5C, 0x24, 0x24, 0x52, 0x02],
2752        "AF Zoom-Nikkor 35-70mm f/2.8D",
2753    ),
2754    (
2755        [0x26, 0x40, 0x3C, 0x5C, 0x2C, 0x34, 0x1C, 0x02],
2756        "AF Zoom-Nikkor 28-70mm f/3.5-4.5D",
2757    ),
2758    (
2759        [0x27, 0x48, 0x8E, 0x8E, 0x24, 0x24, 0x1D, 0x02],
2760        "AF-I Nikkor 300mm f/2.8D IF-ED",
2761    ),
2762    (
2763        [0x27, 0x48, 0x8E, 0x8E, 0x24, 0x24, 0xF1, 0x02],
2764        "AF-I Nikkor 300mm f/2.8D IF-ED + TC-14E",
2765    ),
2766    (
2767        [0x27, 0x48, 0x8E, 0x8E, 0x24, 0x24, 0xE1, 0x02],
2768        "AF-I Nikkor 300mm f/2.8D IF-ED + TC-17E",
2769    ),
2770    (
2771        [0x27, 0x48, 0x8E, 0x8E, 0x24, 0x24, 0xF2, 0x02],
2772        "AF-I Nikkor 300mm f/2.8D IF-ED + TC-20E",
2773    ),
2774    (
2775        [0x28, 0x3C, 0xA6, 0xA6, 0x30, 0x30, 0x1D, 0x02],
2776        "AF-I Nikkor 600mm f/4D IF-ED",
2777    ),
2778    (
2779        [0x28, 0x3C, 0xA6, 0xA6, 0x30, 0x30, 0xF1, 0x02],
2780        "AF-I Nikkor 600mm f/4D IF-ED + TC-14E",
2781    ),
2782    (
2783        [0x28, 0x3C, 0xA6, 0xA6, 0x30, 0x30, 0xE1, 0x02],
2784        "AF-I Nikkor 600mm f/4D IF-ED + TC-17E",
2785    ),
2786    (
2787        [0x28, 0x3C, 0xA6, 0xA6, 0x30, 0x30, 0xF2, 0x02],
2788        "AF-I Nikkor 600mm f/4D IF-ED + TC-20E",
2789    ),
2790    (
2791        [0x2A, 0x54, 0x3C, 0x3C, 0x0C, 0x0C, 0x26, 0x02],
2792        "AF Nikkor 28mm f/1.4D",
2793    ),
2794    (
2795        [0x2B, 0x3C, 0x44, 0x60, 0x30, 0x3C, 0x1F, 0x02],
2796        "AF Zoom-Nikkor 35-80mm f/4-5.6D",
2797    ),
2798    (
2799        [0x2C, 0x48, 0x6A, 0x6A, 0x18, 0x18, 0x27, 0x02],
2800        "AF DC-Nikkor 105mm f/2D",
2801    ),
2802    (
2803        [0x2D, 0x48, 0x80, 0x80, 0x30, 0x30, 0x21, 0x02],
2804        "AF Micro-Nikkor 200mm f/4D IF-ED",
2805    ),
2806    (
2807        [0x2E, 0x48, 0x5C, 0x82, 0x30, 0x3C, 0x22, 0x02],
2808        "AF Nikkor 70-210mm f/4-5.6D",
2809    ),
2810    (
2811        [0x2E, 0x48, 0x5C, 0x82, 0x30, 0x3C, 0x28, 0x02],
2812        "AF Nikkor 70-210mm f/4-5.6D",
2813    ),
2814    (
2815        [0x30, 0x48, 0x98, 0x98, 0x24, 0x24, 0x24, 0x02],
2816        "AF-I Nikkor 400mm f/2.8D IF-ED",
2817    ),
2818    (
2819        [0x30, 0x48, 0x98, 0x98, 0x24, 0x24, 0xF1, 0x02],
2820        "AF-I Nikkor 400mm f/2.8D IF-ED + TC-14E",
2821    ),
2822    (
2823        [0x30, 0x48, 0x98, 0x98, 0x24, 0x24, 0xE1, 0x02],
2824        "AF-I Nikkor 400mm f/2.8D IF-ED + TC-17E",
2825    ),
2826    (
2827        [0x30, 0x48, 0x98, 0x98, 0x24, 0x24, 0xF2, 0x02],
2828        "AF-I Nikkor 400mm f/2.8D IF-ED + TC-20E",
2829    ),
2830    (
2831        [0x31, 0x54, 0x56, 0x56, 0x24, 0x24, 0x25, 0x02],
2832        "AF Micro-Nikkor 60mm f/2.8D",
2833    ),
2834    (
2835        [0x33, 0x48, 0x2D, 0x2D, 0x24, 0x24, 0x31, 0x02],
2836        "AF Nikkor 18mm f/2.8D",
2837    ),
2838    (
2839        [0x34, 0x48, 0x29, 0x29, 0x24, 0x24, 0x32, 0x02],
2840        "AF Fisheye Nikkor 16mm f/2.8D",
2841    ),
2842    (
2843        [0x35, 0x3C, 0xA0, 0xA0, 0x30, 0x30, 0x33, 0x02],
2844        "AF-I Nikkor 500mm f/4D IF-ED",
2845    ),
2846    (
2847        [0x35, 0x3C, 0xA0, 0xA0, 0x30, 0x30, 0xF1, 0x02],
2848        "AF-I Nikkor 500mm f/4D IF-ED + TC-14E",
2849    ),
2850    (
2851        [0x35, 0x3C, 0xA0, 0xA0, 0x30, 0x30, 0xE1, 0x02],
2852        "AF-I Nikkor 500mm f/4D IF-ED + TC-17E",
2853    ),
2854    (
2855        [0x35, 0x3C, 0xA0, 0xA0, 0x30, 0x30, 0xF2, 0x02],
2856        "AF-I Nikkor 500mm f/4D IF-ED + TC-20E",
2857    ),
2858    (
2859        [0x36, 0x48, 0x37, 0x37, 0x24, 0x24, 0x34, 0x02],
2860        "AF Nikkor 24mm f/2.8D",
2861    ),
2862    (
2863        [0x37, 0x48, 0x30, 0x30, 0x24, 0x24, 0x36, 0x02],
2864        "AF Nikkor 20mm f/2.8D",
2865    ),
2866    (
2867        [0x38, 0x4C, 0x62, 0x62, 0x14, 0x14, 0x37, 0x02],
2868        "AF Nikkor 85mm f/1.8D",
2869    ),
2870    (
2871        [0x3A, 0x40, 0x3C, 0x5C, 0x2C, 0x34, 0x39, 0x02],
2872        "AF Zoom-Nikkor 28-70mm f/3.5-4.5D",
2873    ),
2874    (
2875        [0x3B, 0x48, 0x44, 0x5C, 0x24, 0x24, 0x3A, 0x02],
2876        "AF Zoom-Nikkor 35-70mm f/2.8D N",
2877    ),
2878    (
2879        [0x3C, 0x48, 0x60, 0x80, 0x24, 0x24, 0x3B, 0x02],
2880        "AF Zoom-Nikkor 80-200mm f/2.8D ED",
2881    ),
2882    (
2883        [0x3D, 0x3C, 0x44, 0x60, 0x30, 0x3C, 0x3E, 0x02],
2884        "AF Zoom-Nikkor 35-80mm f/4-5.6D",
2885    ),
2886    (
2887        [0x3E, 0x48, 0x3C, 0x3C, 0x24, 0x24, 0x3D, 0x02],
2888        "AF Nikkor 28mm f/2.8D",
2889    ),
2890    (
2891        [0x3F, 0x40, 0x44, 0x6A, 0x2C, 0x34, 0x45, 0x02],
2892        "AF Zoom-Nikkor 35-105mm f/3.5-4.5D",
2893    ),
2894    (
2895        [0x41, 0x48, 0x7C, 0x7C, 0x24, 0x24, 0x43, 0x02],
2896        "AF Nikkor 180mm f/2.8D IF-ED",
2897    ),
2898    (
2899        [0x42, 0x54, 0x44, 0x44, 0x18, 0x18, 0x44, 0x02],
2900        "AF Nikkor 35mm f/2D",
2901    ),
2902    (
2903        [0x43, 0x54, 0x50, 0x50, 0x0C, 0x0C, 0x46, 0x02],
2904        "AF Nikkor 50mm f/1.4D",
2905    ),
2906    (
2907        [0x44, 0x44, 0x60, 0x80, 0x34, 0x3C, 0x47, 0x02],
2908        "AF Zoom-Nikkor 80-200mm f/4.5-5.6D",
2909    ),
2910    (
2911        [0x45, 0x40, 0x3C, 0x60, 0x2C, 0x3C, 0x48, 0x02],
2912        "AF Zoom-Nikkor 28-80mm f/3.5-5.6D",
2913    ),
2914    (
2915        [0x46, 0x3C, 0x44, 0x60, 0x30, 0x3C, 0x49, 0x02],
2916        "AF Zoom-Nikkor 35-80mm f/4-5.6D N",
2917    ),
2918    (
2919        [0x47, 0x42, 0x37, 0x50, 0x2A, 0x34, 0x4A, 0x02],
2920        "AF Zoom-Nikkor 24-50mm f/3.3-4.5D",
2921    ),
2922    (
2923        [0x48, 0x48, 0x8E, 0x8E, 0x24, 0x24, 0x4B, 0x02],
2924        "AF-S Nikkor 300mm f/2.8D IF-ED",
2925    ),
2926    (
2927        [0x48, 0x48, 0x8E, 0x8E, 0x24, 0x24, 0xF1, 0x02],
2928        "AF-S Nikkor 300mm f/2.8D IF-ED + TC-14E",
2929    ),
2930    (
2931        [0x48, 0x48, 0x8E, 0x8E, 0x24, 0x24, 0xE1, 0x02],
2932        "AF-S Nikkor 300mm f/2.8D IF-ED + TC-17E",
2933    ),
2934    (
2935        [0x48, 0x48, 0x8E, 0x8E, 0x24, 0x24, 0xF2, 0x02],
2936        "AF-S Nikkor 300mm f/2.8D IF-ED + TC-20E",
2937    ),
2938    (
2939        [0x49, 0x3C, 0xA6, 0xA6, 0x30, 0x30, 0x4C, 0x02],
2940        "AF-S Nikkor 600mm f/4D IF-ED",
2941    ),
2942    (
2943        [0x49, 0x3C, 0xA6, 0xA6, 0x30, 0x30, 0xF1, 0x02],
2944        "AF-S Nikkor 600mm f/4D IF-ED + TC-14E",
2945    ),
2946    (
2947        [0x49, 0x3C, 0xA6, 0xA6, 0x30, 0x30, 0xE1, 0x02],
2948        "AF-S Nikkor 600mm f/4D IF-ED + TC-17E",
2949    ),
2950    (
2951        [0x49, 0x3C, 0xA6, 0xA6, 0x30, 0x30, 0xF2, 0x02],
2952        "AF-S Nikkor 600mm f/4D IF-ED + TC-20E",
2953    ),
2954    (
2955        [0x4A, 0x54, 0x62, 0x62, 0x0C, 0x0C, 0x4D, 0x02],
2956        "AF Nikkor 85mm f/1.4D IF",
2957    ),
2958    (
2959        [0x4B, 0x3C, 0xA0, 0xA0, 0x30, 0x30, 0x4E, 0x02],
2960        "AF-S Nikkor 500mm f/4D IF-ED",
2961    ),
2962    (
2963        [0x4B, 0x3C, 0xA0, 0xA0, 0x30, 0x30, 0xF1, 0x02],
2964        "AF-S Nikkor 500mm f/4D IF-ED + TC-14E",
2965    ),
2966    (
2967        [0x4B, 0x3C, 0xA0, 0xA0, 0x30, 0x30, 0xE1, 0x02],
2968        "AF-S Nikkor 500mm f/4D IF-ED + TC-17E",
2969    ),
2970    (
2971        [0x4B, 0x3C, 0xA0, 0xA0, 0x30, 0x30, 0xF2, 0x02],
2972        "AF-S Nikkor 500mm f/4D IF-ED + TC-20E",
2973    ),
2974    (
2975        [0x4C, 0x40, 0x37, 0x6E, 0x2C, 0x3C, 0x4F, 0x02],
2976        "AF Zoom-Nikkor 24-120mm f/3.5-5.6D IF",
2977    ),
2978    (
2979        [0x4D, 0x40, 0x3C, 0x80, 0x2C, 0x3C, 0x62, 0x02],
2980        "AF Zoom-Nikkor 28-200mm f/3.5-5.6D IF",
2981    ),
2982    (
2983        [0x4E, 0x48, 0x72, 0x72, 0x18, 0x18, 0x51, 0x02],
2984        "AF DC-Nikkor 135mm f/2D",
2985    ),
2986    (
2987        [0x4F, 0x40, 0x37, 0x5C, 0x2C, 0x3C, 0x53, 0x06],
2988        "IX-Nikkor 24-70mm f/3.5-5.6",
2989    ),
2990    (
2991        [0x50, 0x48, 0x56, 0x7C, 0x30, 0x3C, 0x54, 0x06],
2992        "IX-Nikkor 60-180mm f/4-5.6",
2993    ),
2994    (
2995        [0x53, 0x48, 0x60, 0x80, 0x24, 0x24, 0x57, 0x02],
2996        "AF Zoom-Nikkor 80-200mm f/2.8D ED",
2997    ),
2998    (
2999        [0x53, 0x48, 0x60, 0x80, 0x24, 0x24, 0x60, 0x02],
3000        "AF Zoom-Nikkor 80-200mm f/2.8D ED",
3001    ),
3002    (
3003        [0x54, 0x44, 0x5C, 0x7C, 0x34, 0x3C, 0x58, 0x02],
3004        "AF Zoom-Micro Nikkor 70-180mm f/4.5-5.6D ED",
3005    ),
3006    (
3007        [0x54, 0x44, 0x5C, 0x7C, 0x34, 0x3C, 0x61, 0x02],
3008        "AF Zoom-Micro Nikkor 70-180mm f/4.5-5.6D ED",
3009    ),
3010    (
3011        [0x56, 0x48, 0x5C, 0x8E, 0x30, 0x3C, 0x5A, 0x02],
3012        "AF Zoom-Nikkor 70-300mm f/4-5.6D ED",
3013    ),
3014    (
3015        [0x59, 0x48, 0x98, 0x98, 0x24, 0x24, 0x5D, 0x02],
3016        "AF-S Nikkor 400mm f/2.8D IF-ED",
3017    ),
3018    (
3019        [0x59, 0x48, 0x98, 0x98, 0x24, 0x24, 0xF1, 0x02],
3020        "AF-S Nikkor 400mm f/2.8D IF-ED + TC-14E",
3021    ),
3022    (
3023        [0x59, 0x48, 0x98, 0x98, 0x24, 0x24, 0xE1, 0x02],
3024        "AF-S Nikkor 400mm f/2.8D IF-ED + TC-17E",
3025    ),
3026    (
3027        [0x59, 0x48, 0x98, 0x98, 0x24, 0x24, 0xF2, 0x02],
3028        "AF-S Nikkor 400mm f/2.8D IF-ED + TC-20E",
3029    ),
3030    (
3031        [0x5A, 0x3C, 0x3E, 0x56, 0x30, 0x3C, 0x5E, 0x06],
3032        "IX-Nikkor 30-60mm f/4-5.6",
3033    ),
3034    (
3035        [0x5B, 0x44, 0x56, 0x7C, 0x34, 0x3C, 0x5F, 0x06],
3036        "IX-Nikkor 60-180mm f/4.5-5.6",
3037    ),
3038    (
3039        [0x5D, 0x48, 0x3C, 0x5C, 0x24, 0x24, 0x63, 0x02],
3040        "AF-S Zoom-Nikkor 28-70mm f/2.8D IF-ED",
3041    ),
3042    (
3043        [0x5E, 0x48, 0x60, 0x80, 0x24, 0x24, 0x64, 0x02],
3044        "AF-S Zoom-Nikkor 80-200mm f/2.8D IF-ED",
3045    ),
3046    (
3047        [0x5F, 0x40, 0x3C, 0x6A, 0x2C, 0x34, 0x65, 0x02],
3048        "AF Zoom-Nikkor 28-105mm f/3.5-4.5D IF",
3049    ),
3050    (
3051        [0x60, 0x40, 0x3C, 0x60, 0x2C, 0x3C, 0x66, 0x02],
3052        "AF Zoom-Nikkor 28-80mm f/3.5-5.6D",
3053    ),
3054    (
3055        [0x61, 0x44, 0x5E, 0x86, 0x34, 0x3C, 0x67, 0x02],
3056        "AF Zoom-Nikkor 75-240mm f/4.5-5.6D",
3057    ),
3058    (
3059        [0x63, 0x48, 0x2B, 0x44, 0x24, 0x24, 0x68, 0x02],
3060        "AF-S Nikkor 17-35mm f/2.8D IF-ED",
3061    ),
3062    (
3063        [0x64, 0x00, 0x62, 0x62, 0x24, 0x24, 0x6A, 0x02],
3064        "PC Micro-Nikkor 85mm f/2.8D",
3065    ),
3066    (
3067        [0x65, 0x44, 0x60, 0x98, 0x34, 0x3C, 0x6B, 0x0A],
3068        "AF VR Zoom-Nikkor 80-400mm f/4.5-5.6D ED",
3069    ),
3070    (
3071        [0x66, 0x40, 0x2D, 0x44, 0x2C, 0x34, 0x6C, 0x02],
3072        "AF Zoom-Nikkor 18-35mm f/3.5-4.5D IF-ED",
3073    ),
3074    (
3075        [0x67, 0x48, 0x37, 0x62, 0x24, 0x30, 0x6D, 0x02],
3076        "AF Zoom-Nikkor 24-85mm f/2.8-4D IF",
3077    ),
3078    (
3079        [0x68, 0x42, 0x3C, 0x60, 0x2A, 0x3C, 0x6E, 0x06],
3080        "AF Zoom-Nikkor 28-80mm f/3.3-5.6G",
3081    ),
3082    (
3083        [0x69, 0x48, 0x5C, 0x8E, 0x30, 0x3C, 0x6F, 0x06],
3084        "AF Zoom-Nikkor 70-300mm f/4-5.6G",
3085    ),
3086    (
3087        [0x6A, 0x48, 0x8E, 0x8E, 0x30, 0x30, 0x70, 0x02],
3088        "AF-S Nikkor 300mm f/4D IF-ED",
3089    ),
3090    (
3091        [0x6B, 0x48, 0x24, 0x24, 0x24, 0x24, 0x71, 0x02],
3092        "AF Nikkor ED 14mm f/2.8D",
3093    ),
3094    (
3095        [0x6D, 0x48, 0x8E, 0x8E, 0x24, 0x24, 0x73, 0x02],
3096        "AF-S Nikkor 300mm f/2.8D IF-ED II",
3097    ),
3098    (
3099        [0x6E, 0x48, 0x98, 0x98, 0x24, 0x24, 0x74, 0x02],
3100        "AF-S Nikkor 400mm f/2.8D IF-ED II",
3101    ),
3102    (
3103        [0x6F, 0x3C, 0xA0, 0xA0, 0x30, 0x30, 0x75, 0x02],
3104        "AF-S Nikkor 500mm f/4D IF-ED II",
3105    ),
3106    (
3107        [0x70, 0x3C, 0xA6, 0xA6, 0x30, 0x30, 0x76, 0x02],
3108        "AF-S Nikkor 600mm f/4D IF-ED II",
3109    ),
3110    (
3111        [0x72, 0x48, 0x4C, 0x4C, 0x24, 0x24, 0x77, 0x00],
3112        "Nikkor 45mm f/2.8 P",
3113    ),
3114    (
3115        [0x74, 0x40, 0x37, 0x62, 0x2C, 0x34, 0x78, 0x06],
3116        "AF-S Zoom-Nikkor 24-85mm f/3.5-4.5G IF-ED",
3117    ),
3118    (
3119        [0x75, 0x40, 0x3C, 0x68, 0x2C, 0x3C, 0x79, 0x06],
3120        "AF Zoom-Nikkor 28-100mm f/3.5-5.6G",
3121    ),
3122    (
3123        [0x76, 0x58, 0x50, 0x50, 0x14, 0x14, 0x7A, 0x02],
3124        "AF Nikkor 50mm f/1.8D",
3125    ),
3126    (
3127        [0x77, 0x48, 0x5C, 0x80, 0x24, 0x24, 0x7B, 0x0E],
3128        "AF-S VR Zoom-Nikkor 70-200mm f/2.8G IF-ED",
3129    ),
3130    (
3131        [0x78, 0x40, 0x37, 0x6E, 0x2C, 0x3C, 0x7C, 0x0E],
3132        "AF-S VR Zoom-Nikkor 24-120mm f/3.5-5.6G IF-ED",
3133    ),
3134    (
3135        [0x79, 0x40, 0x3C, 0x80, 0x2C, 0x3C, 0x7F, 0x06],
3136        "AF Zoom-Nikkor 28-200mm f/3.5-5.6G IF-ED",
3137    ),
3138    (
3139        [0x7B, 0x48, 0x80, 0x98, 0x30, 0x30, 0x80, 0x0E],
3140        "AF-S VR Zoom-Nikkor 200-400mm f/4G IF-ED",
3141    ),
3142    (
3143        [0x7D, 0x48, 0x2B, 0x53, 0x24, 0x24, 0x82, 0x06],
3144        "AF-S DX Zoom-Nikkor 17-55mm f/2.8G IF-ED",
3145    ),
3146    (
3147        [0x7F, 0x40, 0x2D, 0x5C, 0x2C, 0x34, 0x84, 0x06],
3148        "AF-S DX Zoom-Nikkor 18-70mm f/3.5-4.5G IF-ED",
3149    ),
3150    (
3151        [0x80, 0x48, 0x1A, 0x1A, 0x24, 0x24, 0x85, 0x06],
3152        "AF DX Fisheye-Nikkor 10.5mm f/2.8G ED",
3153    ),
3154    (
3155        [0x81, 0x54, 0x80, 0x80, 0x18, 0x18, 0x86, 0x0E],
3156        "AF-S VR Nikkor 200mm f/2G IF-ED",
3157    ),
3158    (
3159        [0x82, 0x48, 0x8E, 0x8E, 0x24, 0x24, 0x87, 0x0E],
3160        "AF-S VR Nikkor 300mm f/2.8G IF-ED",
3161    ),
3162    (
3163        [0x83, 0x00, 0xB0, 0xB0, 0x5A, 0x5A, 0x88, 0x04],
3164        "FSA-L2, EDG 65, 800mm F13 G",
3165    ),
3166    (
3167        [0x89, 0x3C, 0x53, 0x80, 0x30, 0x3C, 0x8B, 0x06],
3168        "AF-S DX Zoom-Nikkor 55-200mm f/4-5.6G ED",
3169    ),
3170    (
3171        [0x8A, 0x54, 0x6A, 0x6A, 0x24, 0x24, 0x8C, 0x0E],
3172        "AF-S VR Micro-Nikkor 105mm f/2.8G IF-ED",
3173    ),
3174    (
3175        [0x8B, 0x40, 0x2D, 0x80, 0x2C, 0x3C, 0x8D, 0x0E],
3176        "AF-S DX VR Zoom-Nikkor 18-200mm f/3.5-5.6G IF-ED",
3177    ),
3178    (
3179        [0x8B, 0x40, 0x2D, 0x80, 0x2C, 0x3C, 0xFD, 0x0E],
3180        "AF-S DX VR Zoom-Nikkor 18-200mm f/3.5-5.6G IF-ED [II]",
3181    ),
3182    (
3183        [0x8C, 0x40, 0x2D, 0x53, 0x2C, 0x3C, 0x8E, 0x06],
3184        "AF-S DX Zoom-Nikkor 18-55mm f/3.5-5.6G ED",
3185    ),
3186    (
3187        [0x8D, 0x44, 0x5C, 0x8E, 0x34, 0x3C, 0x8F, 0x0E],
3188        "AF-S VR Zoom-Nikkor 70-300mm f/4.5-5.6G IF-ED",
3189    ),
3190    (
3191        [0x8F, 0x40, 0x2D, 0x72, 0x2C, 0x3C, 0x91, 0x06],
3192        "AF-S DX Zoom-Nikkor 18-135mm f/3.5-5.6G IF-ED",
3193    ),
3194    (
3195        [0x90, 0x3B, 0x53, 0x80, 0x30, 0x3C, 0x92, 0x0E],
3196        "AF-S DX VR Zoom-Nikkor 55-200mm f/4-5.6G IF-ED",
3197    ),
3198    (
3199        [0x92, 0x48, 0x24, 0x37, 0x24, 0x24, 0x94, 0x06],
3200        "AF-S Zoom-Nikkor 14-24mm f/2.8G ED",
3201    ),
3202    (
3203        [0x93, 0x48, 0x37, 0x5C, 0x24, 0x24, 0x95, 0x06],
3204        "AF-S Zoom-Nikkor 24-70mm f/2.8G ED",
3205    ),
3206    (
3207        [0x94, 0x40, 0x2D, 0x53, 0x2C, 0x3C, 0x96, 0x06],
3208        "AF-S DX Zoom-Nikkor 18-55mm f/3.5-5.6G ED II",
3209    ),
3210    (
3211        [0x95, 0x4C, 0x37, 0x37, 0x2C, 0x2C, 0x97, 0x02],
3212        "PC-E Nikkor 24mm f/3.5D ED",
3213    ),
3214    (
3215        [0x95, 0x00, 0x37, 0x37, 0x2C, 0x2C, 0x97, 0x06],
3216        "PC-E Nikkor 24mm f/3.5D ED",
3217    ),
3218    (
3219        [0x96, 0x48, 0x98, 0x98, 0x24, 0x24, 0x98, 0x0E],
3220        "AF-S VR Nikkor 400mm f/2.8G ED",
3221    ),
3222    (
3223        [0x97, 0x3C, 0xA0, 0xA0, 0x30, 0x30, 0x99, 0x0E],
3224        "AF-S VR Nikkor 500mm f/4G ED",
3225    ),
3226    (
3227        [0x98, 0x3C, 0xA6, 0xA6, 0x30, 0x30, 0x9A, 0x0E],
3228        "AF-S VR Nikkor 600mm f/4G ED",
3229    ),
3230    (
3231        [0x99, 0x40, 0x29, 0x62, 0x2C, 0x3C, 0x9B, 0x0E],
3232        "AF-S DX VR Zoom-Nikkor 16-85mm f/3.5-5.6G ED",
3233    ),
3234    (
3235        [0x9A, 0x40, 0x2D, 0x53, 0x2C, 0x3C, 0x9C, 0x0E],
3236        "AF-S DX VR Zoom-Nikkor 18-55mm f/3.5-5.6G",
3237    ),
3238    (
3239        [0x9B, 0x54, 0x4C, 0x4C, 0x24, 0x24, 0x9D, 0x02],
3240        "PC-E Micro Nikkor 45mm f/2.8D ED",
3241    ),
3242    (
3243        [0x9B, 0x00, 0x4C, 0x4C, 0x24, 0x24, 0x9D, 0x06],
3244        "PC-E Micro Nikkor 45mm f/2.8D ED",
3245    ),
3246    (
3247        [0x9C, 0x54, 0x56, 0x56, 0x24, 0x24, 0x9E, 0x06],
3248        "AF-S Micro Nikkor 60mm f/2.8G ED",
3249    ),
3250    (
3251        [0x9D, 0x54, 0x62, 0x62, 0x24, 0x24, 0x9F, 0x02],
3252        "PC-E Micro Nikkor 85mm f/2.8D",
3253    ),
3254    (
3255        [0x9D, 0x00, 0x62, 0x62, 0x24, 0x24, 0x9F, 0x06],
3256        "PC-E Micro Nikkor 85mm f/2.8D",
3257    ),
3258    (
3259        [0x9E, 0x40, 0x2D, 0x6A, 0x2C, 0x3C, 0xA0, 0x0E],
3260        "AF-S DX VR Zoom-Nikkor 18-105mm f/3.5-5.6G ED",
3261    ),
3262    (
3263        [0x9F, 0x58, 0x44, 0x44, 0x14, 0x14, 0xA1, 0x06],
3264        "AF-S DX Nikkor 35mm f/1.8G",
3265    ),
3266    (
3267        [0xA0, 0x54, 0x50, 0x50, 0x0C, 0x0C, 0xA2, 0x06],
3268        "AF-S Nikkor 50mm f/1.4G",
3269    ),
3270    (
3271        [0xA1, 0x40, 0x18, 0x37, 0x2C, 0x34, 0xA3, 0x06],
3272        "AF-S DX Nikkor 10-24mm f/3.5-4.5G ED",
3273    ),
3274    (
3275        [0xA1, 0x40, 0x2D, 0x53, 0x2C, 0x3C, 0xCB, 0x86],
3276        "AF-P DX Nikkor 18-55mm f/3.5-5.6G",
3277    ),
3278    (
3279        [0xA2, 0x48, 0x5C, 0x80, 0x24, 0x24, 0xA4, 0x0E],
3280        "AF-S Nikkor 70-200mm f/2.8G ED VR II",
3281    ),
3282    (
3283        [0xA3, 0x3C, 0x29, 0x44, 0x30, 0x30, 0xA5, 0x0E],
3284        "AF-S Nikkor 16-35mm f/4G ED VR",
3285    ),
3286    (
3287        [0xA4, 0x54, 0x37, 0x37, 0x0C, 0x0C, 0xA6, 0x06],
3288        "AF-S Nikkor 24mm f/1.4G ED",
3289    ),
3290    (
3291        [0xA5, 0x40, 0x3C, 0x8E, 0x2C, 0x3C, 0xA7, 0x0E],
3292        "AF-S Nikkor 28-300mm f/3.5-5.6G ED VR",
3293    ),
3294    (
3295        [0xA6, 0x48, 0x8E, 0x8E, 0x24, 0x24, 0xA8, 0x0E],
3296        "AF-S Nikkor 300mm f/2.8G IF-ED VR II",
3297    ),
3298    (
3299        [0xA7, 0x4B, 0x62, 0x62, 0x2C, 0x2C, 0xA9, 0x0E],
3300        "AF-S DX Micro Nikkor 85mm f/3.5G ED VR",
3301    ),
3302    (
3303        [0xA8, 0x48, 0x80, 0x98, 0x30, 0x30, 0xAA, 0x0E],
3304        "AF-S Zoom-Nikkor 200-400mm f/4G IF-ED VR II",
3305    ),
3306    (
3307        [0xA9, 0x54, 0x80, 0x80, 0x18, 0x18, 0xAB, 0x0E],
3308        "AF-S Nikkor 200mm f/2G ED VR II",
3309    ),
3310    (
3311        [0xAA, 0x3C, 0x37, 0x6E, 0x30, 0x30, 0xAC, 0x0E],
3312        "AF-S Nikkor 24-120mm f/4G ED VR",
3313    ),
3314    (
3315        [0xAC, 0x38, 0x53, 0x8E, 0x34, 0x3C, 0xAE, 0x0E],
3316        "AF-S DX Nikkor 55-300mm f/4.5-5.6G ED VR",
3317    ),
3318    (
3319        [0xAD, 0x3C, 0x2D, 0x8E, 0x2C, 0x3C, 0xAF, 0x0E],
3320        "AF-S DX Nikkor 18-300mm f/3.5-5.6G ED VR",
3321    ),
3322    (
3323        [0xAE, 0x54, 0x62, 0x62, 0x0C, 0x0C, 0xB0, 0x06],
3324        "AF-S Nikkor 85mm f/1.4G",
3325    ),
3326    (
3327        [0xAF, 0x54, 0x44, 0x44, 0x0C, 0x0C, 0xB1, 0x06],
3328        "AF-S Nikkor 35mm f/1.4G",
3329    ),
3330    (
3331        [0xB0, 0x4C, 0x50, 0x50, 0x14, 0x14, 0xB2, 0x06],
3332        "AF-S Nikkor 50mm f/1.8G",
3333    ),
3334    (
3335        [0xB1, 0x48, 0x48, 0x48, 0x24, 0x24, 0xB3, 0x06],
3336        "AF-S DX Micro Nikkor 40mm f/2.8G",
3337    ),
3338    (
3339        [0xB2, 0x48, 0x5C, 0x80, 0x30, 0x30, 0xB4, 0x0E],
3340        "AF-S Nikkor 70-200mm f/4G ED VR",
3341    ),
3342    (
3343        [0xB3, 0x4C, 0x62, 0x62, 0x14, 0x14, 0xB5, 0x06],
3344        "AF-S Nikkor 85mm f/1.8G",
3345    ),
3346    (
3347        [0xB4, 0x40, 0x37, 0x62, 0x2C, 0x34, 0xB6, 0x0E],
3348        "AF-S Zoom-Nikkor 24-85mm f/3.5-4.5G IF-ED VR",
3349    ),
3350    (
3351        [0xB5, 0x4C, 0x3C, 0x3C, 0x14, 0x14, 0xB7, 0x06],
3352        "AF-S Nikkor 28mm f/1.8G",
3353    ),
3354    (
3355        [0xB6, 0x3C, 0xB0, 0xB0, 0x3C, 0x3C, 0xB8, 0x0E],
3356        "AF-S VR Nikkor 800mm f/5.6E FL ED",
3357    ),
3358    (
3359        [0xB6, 0x3C, 0xB0, 0xB0, 0x3C, 0x3C, 0xB8, 0x4E],
3360        "AF-S VR Nikkor 800mm f/5.6E FL ED",
3361    ),
3362    (
3363        [0xB7, 0x44, 0x60, 0x98, 0x34, 0x3C, 0xB9, 0x0E],
3364        "AF-S Nikkor 80-400mm f/4.5-5.6G ED VR",
3365    ),
3366    (
3367        [0xB8, 0x40, 0x2D, 0x44, 0x2C, 0x34, 0xBA, 0x06],
3368        "AF-S Nikkor 18-35mm f/3.5-4.5G ED",
3369    ),
3370    (
3371        [0xA0, 0x40, 0x2D, 0x74, 0x2C, 0x3C, 0xBB, 0x0E],
3372        "AF-S DX Nikkor 18-140mm f/3.5-5.6G ED VR",
3373    ),
3374    (
3375        [0xA1, 0x54, 0x55, 0x55, 0x0C, 0x0C, 0xBC, 0x06],
3376        "AF-S Nikkor 58mm f/1.4G",
3377    ),
3378    (
3379        [0xA1, 0x48, 0x6E, 0x8E, 0x24, 0x24, 0xDB, 0x4E],
3380        "AF-S Nikkor 120-300mm f/2.8E FL ED SR VR",
3381    ),
3382    (
3383        [0xA2, 0x40, 0x2D, 0x53, 0x2C, 0x3C, 0xBD, 0x0E],
3384        "AF-S DX Nikkor 18-55mm f/3.5-5.6G VR II",
3385    ),
3386    (
3387        [0xA4, 0x40, 0x2D, 0x8E, 0x2C, 0x40, 0xBF, 0x0E],
3388        "AF-S DX Nikkor 18-300mm f/3.5-6.3G ED VR",
3389    ),
3390    (
3391        [0xA5, 0x4C, 0x44, 0x44, 0x14, 0x14, 0xC0, 0x06],
3392        "AF-S Nikkor 35mm f/1.8G ED",
3393    ),
3394    (
3395        [0xA6, 0x48, 0x98, 0x98, 0x24, 0x24, 0xC1, 0x0E],
3396        "AF-S Nikkor 400mm f/2.8E FL ED VR",
3397    ),
3398    (
3399        [0xA7, 0x3C, 0x53, 0x80, 0x30, 0x3C, 0xC2, 0x0E],
3400        "AF-S DX Nikkor 55-200mm f/4-5.6G ED VR II",
3401    ),
3402    (
3403        [0xA8, 0x48, 0x8E, 0x8E, 0x30, 0x30, 0xC3, 0x4E],
3404        "AF-S Nikkor 300mm f/4E PF ED VR",
3405    ),
3406    (
3407        [0xA8, 0x48, 0x8E, 0x8E, 0x30, 0x30, 0xC3, 0x0E],
3408        "AF-S Nikkor 300mm f/4E PF ED VR",
3409    ),
3410    (
3411        [0xA9, 0x4C, 0x31, 0x31, 0x14, 0x14, 0xC4, 0x06],
3412        "AF-S Nikkor 20mm f/1.8G ED",
3413    ),
3414    (
3415        [0xAA, 0x48, 0x37, 0x5C, 0x24, 0x24, 0xC5, 0x4E],
3416        "AF-S Nikkor 24-70mm f/2.8E ED VR",
3417    ),
3418    (
3419        [0xAA, 0x48, 0x37, 0x5C, 0x24, 0x24, 0xC5, 0x0E],
3420        "AF-S Nikkor 24-70mm f/2.8E ED VR",
3421    ),
3422    (
3423        [0xAB, 0x3C, 0xA0, 0xA0, 0x30, 0x30, 0xC6, 0x4E],
3424        "AF-S Nikkor 500mm f/4E FL ED VR",
3425    ),
3426    (
3427        [0xAC, 0x3C, 0xA6, 0xA6, 0x30, 0x30, 0xC7, 0x4E],
3428        "AF-S Nikkor 600mm f/4E FL ED VR",
3429    ),
3430    (
3431        [0xAD, 0x48, 0x28, 0x60, 0x24, 0x30, 0xC8, 0x4E],
3432        "AF-S DX Nikkor 16-80mm f/2.8-4E ED VR",
3433    ),
3434    (
3435        [0xAD, 0x48, 0x28, 0x60, 0x24, 0x30, 0xC8, 0x0E],
3436        "AF-S DX Nikkor 16-80mm f/2.8-4E ED VR",
3437    ),
3438    (
3439        [0xAE, 0x3C, 0x80, 0xA0, 0x3C, 0x3C, 0xC9, 0x4E],
3440        "AF-S Nikkor 200-500mm f/5.6E ED VR",
3441    ),
3442    (
3443        [0xAE, 0x3C, 0x80, 0xA0, 0x3C, 0x3C, 0xC9, 0x0E],
3444        "AF-S Nikkor 200-500mm f/5.6E ED VR",
3445    ),
3446    (
3447        [0xA0, 0x40, 0x2D, 0x53, 0x2C, 0x3C, 0xCA, 0x8E],
3448        "AF-P DX Nikkor 18-55mm f/3.5-5.6G",
3449    ),
3450    (
3451        [0xA0, 0x40, 0x2D, 0x53, 0x2C, 0x3C, 0xCA, 0x0E],
3452        "AF-P DX Nikkor 18-55mm f/3.5-5.6G VR",
3453    ),
3454    (
3455        [0xAF, 0x4C, 0x37, 0x37, 0x14, 0x14, 0xCC, 0x06],
3456        "AF-S Nikkor 24mm f/1.8G ED",
3457    ),
3458    (
3459        [0xA2, 0x38, 0x5C, 0x8E, 0x34, 0x40, 0xCD, 0x86],
3460        "AF-P DX Nikkor 70-300mm f/4.5-6.3G VR",
3461    ),
3462    (
3463        [0xA3, 0x38, 0x5C, 0x8E, 0x34, 0x40, 0xCE, 0x8E],
3464        "AF-P DX Nikkor 70-300mm f/4.5-6.3G ED VR",
3465    ),
3466    (
3467        [0xA3, 0x38, 0x5C, 0x8E, 0x34, 0x40, 0xCE, 0x0E],
3468        "AF-P DX Nikkor 70-300mm f/4.5-6.3G ED",
3469    ),
3470    (
3471        [0xA4, 0x48, 0x5C, 0x80, 0x24, 0x24, 0xCF, 0x4E],
3472        "AF-S Nikkor 70-200mm f/2.8E FL ED VR",
3473    ),
3474    (
3475        [0xA4, 0x48, 0x5C, 0x80, 0x24, 0x24, 0xCF, 0x0E],
3476        "AF-S Nikkor 70-200mm f/2.8E FL ED VR",
3477    ),
3478    (
3479        [0xA5, 0x54, 0x6A, 0x6A, 0x0C, 0x0C, 0xD0, 0x46],
3480        "AF-S Nikkor 105mm f/1.4E ED",
3481    ),
3482    (
3483        [0xA5, 0x54, 0x6A, 0x6A, 0x0C, 0x0C, 0xD0, 0x06],
3484        "AF-S Nikkor 105mm f/1.4E ED",
3485    ),
3486    (
3487        [0xA6, 0x48, 0x2F, 0x2F, 0x30, 0x30, 0xD1, 0x46],
3488        "PC Nikkor 19mm f/4E ED",
3489    ),
3490    (
3491        [0xA6, 0x48, 0x2F, 0x2F, 0x30, 0x30, 0xD1, 0x06],
3492        "PC Nikkor 19mm f/4E ED",
3493    ),
3494    (
3495        [0xA7, 0x40, 0x11, 0x26, 0x2C, 0x34, 0xD2, 0x46],
3496        "AF-S Fisheye Nikkor 8-15mm f/3.5-4.5E ED",
3497    ),
3498    (
3499        [0xA7, 0x40, 0x11, 0x26, 0x2C, 0x34, 0xD2, 0x06],
3500        "AF-S Fisheye Nikkor 8-15mm f/3.5-4.5E ED",
3501    ),
3502    (
3503        [0xA8, 0x38, 0x18, 0x30, 0x34, 0x3C, 0xD3, 0x8E],
3504        "AF-P DX Nikkor 10-20mm f/4.5-5.6G VR",
3505    ),
3506    (
3507        [0xA8, 0x38, 0x18, 0x30, 0x34, 0x3C, 0xD3, 0x0E],
3508        "AF-P DX Nikkor 10-20mm f/4.5-5.6G VR",
3509    ),
3510    (
3511        [0xA9, 0x48, 0x7C, 0x98, 0x30, 0x30, 0xD4, 0x4E],
3512        "AF-S Nikkor 180-400mm f/4E TC1.4 FL ED VR",
3513    ),
3514    (
3515        [0xA9, 0x48, 0x7C, 0x98, 0x30, 0x30, 0xD4, 0x0E],
3516        "AF-S Nikkor 180-400mm f/4E TC1.4 FL ED VR",
3517    ),
3518    (
3519        [0xAA, 0x48, 0x88, 0xA4, 0x3C, 0x3C, 0xD5, 0x4E],
3520        "AF-S Nikkor 180-400mm f/4E TC1.4 FL ED VR + 1.4x TC",
3521    ),
3522    (
3523        [0xAA, 0x48, 0x88, 0xA4, 0x3C, 0x3C, 0xD5, 0x0E],
3524        "AF-S Nikkor 180-400mm f/4E TC1.4 FL ED VR + 1.4x TC",
3525    ),
3526    (
3527        [0xAB, 0x44, 0x5C, 0x8E, 0x34, 0x3C, 0xD6, 0xCE],
3528        "AF-P Nikkor 70-300mm f/4.5-5.6E ED VR",
3529    ),
3530    (
3531        [0xAB, 0x44, 0x5C, 0x8E, 0x34, 0x3C, 0xD6, 0x0E],
3532        "AF-P Nikkor 70-300mm f/4.5-5.6E ED VR",
3533    ),
3534    (
3535        [0xAB, 0x44, 0x5C, 0x8E, 0x34, 0x3C, 0xD6, 0x4E],
3536        "AF-P Nikkor 70-300mm f/4.5-5.6E ED VR",
3537    ),
3538    (
3539        [0xAC, 0x54, 0x3C, 0x3C, 0x0C, 0x0C, 0xD7, 0x46],
3540        "AF-S Nikkor 28mm f/1.4E ED",
3541    ),
3542    (
3543        [0xAC, 0x54, 0x3C, 0x3C, 0x0C, 0x0C, 0xD7, 0x06],
3544        "AF-S Nikkor 28mm f/1.4E ED",
3545    ),
3546    (
3547        [0xAD, 0x3C, 0xA0, 0xA0, 0x3C, 0x3C, 0xD8, 0x0E],
3548        "AF-S Nikkor 500mm f/5.6E PF ED VR",
3549    ),
3550    (
3551        [0xAD, 0x3C, 0xA0, 0xA0, 0x3C, 0x3C, 0xD8, 0x4E],
3552        "AF-S Nikkor 500mm f/5.6E PF ED VR",
3553    ),
3554    ([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00], "TC-16A"),
3555    ([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00], "TC-16A"),
3556    (
3557        [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF1, 0x0C],
3558        "TC-14E [II] or Sigma APO Tele Converter 1.4x EX DG or Kenko Teleplus PRO 300 DG 1.4x",
3559    ),
3560    (
3561        [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF2, 0x18],
3562        "TC-20E [II] or Sigma APO Tele Converter 2x EX DG or Kenko Teleplus PRO 300 DG 2.0x",
3563    ),
3564    (
3565        [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE1, 0x12],
3566        "TC-17E II",
3567    ),
3568    (
3569        [0xFE, 0x47, 0x00, 0x00, 0x24, 0x24, 0x4B, 0x06],
3570        "Sigma 4.5mm F2.8 EX DC HSM Circular Fisheye",
3571    ),
3572    (
3573        [0x26, 0x48, 0x11, 0x11, 0x30, 0x30, 0x1C, 0x02],
3574        "Sigma 8mm F4 EX Circular Fisheye",
3575    ),
3576    (
3577        [0x79, 0x40, 0x11, 0x11, 0x2C, 0x2C, 0x1C, 0x06],
3578        "Sigma 8mm F3.5 EX Circular Fisheye",
3579    ),
3580    (
3581        [0xDB, 0x40, 0x11, 0x11, 0x2C, 0x2C, 0x1C, 0x06],
3582        "Sigma 8mm F3.5 EX DG Circular Fisheye",
3583    ),
3584    (
3585        [0xDC, 0x48, 0x19, 0x19, 0x24, 0x24, 0x4B, 0x06],
3586        "Sigma 10mm F2.8 EX DC HSM Fisheye",
3587    ),
3588    (
3589        [0xC2, 0x4C, 0x24, 0x24, 0x14, 0x14, 0x4B, 0x06],
3590        "Sigma 14mm F1.8 DG HSM | A",
3591    ),
3592    (
3593        [0x48, 0x48, 0x24, 0x24, 0x24, 0x24, 0x4B, 0x02],
3594        "Sigma 14mm F2.8 EX Aspherical HSM",
3595    ),
3596    (
3597        [0x02, 0x3F, 0x24, 0x24, 0x2C, 0x2C, 0x02, 0x00],
3598        "Sigma 14mm F3.5",
3599    ),
3600    (
3601        [0x26, 0x48, 0x27, 0x27, 0x24, 0x24, 0x1C, 0x02],
3602        "Sigma 15mm F2.8 EX Diagonal Fisheye",
3603    ),
3604    (
3605        [0xEA, 0x48, 0x27, 0x27, 0x24, 0x24, 0x1C, 0x02],
3606        "Sigma 15mm F2.8 EX Diagonal Fisheye",
3607    ),
3608    (
3609        [0x26, 0x58, 0x31, 0x31, 0x14, 0x14, 0x1C, 0x02],
3610        "Sigma 20mm F1.8 EX DG Aspherical RF",
3611    ),
3612    (
3613        [0x79, 0x54, 0x31, 0x31, 0x0C, 0x0C, 0x4B, 0x06],
3614        "Sigma 20mm F1.4 DG HSM | A",
3615    ),
3616    (
3617        [0x26, 0x58, 0x37, 0x37, 0x14, 0x14, 0x1C, 0x02],
3618        "Sigma 24mm F1.8 EX DG Aspherical Macro",
3619    ),
3620    (
3621        [0xE1, 0x58, 0x37, 0x37, 0x14, 0x14, 0x1C, 0x02],
3622        "Sigma 24mm F1.8 EX DG Aspherical Macro",
3623    ),
3624    (
3625        [0x02, 0x46, 0x37, 0x37, 0x25, 0x25, 0x02, 0x00],
3626        "Sigma 24mm F2.8 Super Wide II Macro",
3627    ),
3628    (
3629        [0x7E, 0x54, 0x37, 0x37, 0x0C, 0x0C, 0x4B, 0x06],
3630        "Sigma 24mm F1.4 DG HSM | A",
3631    ),
3632    (
3633        [0x26, 0x58, 0x3C, 0x3C, 0x14, 0x14, 0x1C, 0x02],
3634        "Sigma 28mm F1.8 EX DG Aspherical Macro",
3635    ),
3636    (
3637        [0xBC, 0x54, 0x3C, 0x3C, 0x0C, 0x0C, 0x4B, 0x46],
3638        "Sigma 28mm F1.4 DG HSM | A",
3639    ),
3640    (
3641        [0x48, 0x54, 0x3E, 0x3E, 0x0C, 0x0C, 0x4B, 0x06],
3642        "Sigma 30mm F1.4 EX DC HSM",
3643    ),
3644    (
3645        [0xF8, 0x54, 0x3E, 0x3E, 0x0C, 0x0C, 0x4B, 0x06],
3646        "Sigma 30mm F1.4 EX DC HSM",
3647    ),
3648    (
3649        [0x91, 0x54, 0x44, 0x44, 0x0C, 0x0C, 0x4B, 0x06],
3650        "Sigma 35mm F1.4 DG HSM",
3651    ),
3652    (
3653        [0xBD, 0x54, 0x48, 0x48, 0x0C, 0x0C, 0x4B, 0x46],
3654        "Sigma 40mm F1.4 DG HSM | A",
3655    ),
3656    (
3657        [0xDE, 0x54, 0x50, 0x50, 0x0C, 0x0C, 0x4B, 0x06],
3658        "Sigma 50mm F1.4 EX DG HSM",
3659    ),
3660    (
3661        [0x88, 0x54, 0x50, 0x50, 0x0C, 0x0C, 0x4B, 0x06],
3662        "Sigma 50mm F1.4 DG HSM | A",
3663    ),
3664    (
3665        [0x02, 0x48, 0x50, 0x50, 0x24, 0x24, 0x02, 0x00],
3666        "Sigma Macro 50mm F2.8",
3667    ),
3668    (
3669        [0x32, 0x54, 0x50, 0x50, 0x24, 0x24, 0x35, 0x02],
3670        "Sigma Macro 50mm F2.8 EX DG",
3671    ),
3672    (
3673        [0xE3, 0x54, 0x50, 0x50, 0x24, 0x24, 0x35, 0x02],
3674        "Sigma Macro 50mm F2.8 EX DG",
3675    ),
3676    (
3677        [0x79, 0x48, 0x5C, 0x5C, 0x24, 0x24, 0x1C, 0x06],
3678        "Sigma Macro 70mm F2.8 EX DG",
3679    ),
3680    (
3681        [0x9B, 0x54, 0x62, 0x62, 0x0C, 0x0C, 0x4B, 0x06],
3682        "Sigma 85mm F1.4 EX DG HSM",
3683    ),
3684    (
3685        [0xC8, 0x54, 0x62, 0x62, 0x0C, 0x0C, 0x4B, 0x46],
3686        "Sigma 85mm F1.4 DG HSM | A",
3687    ),
3688    (
3689        [0xC8, 0x54, 0x62, 0x62, 0x0C, 0x0C, 0x4B, 0x06],
3690        "Sigma 85mm F1.4 DG HSM | A",
3691    ),
3692    (
3693        [0x02, 0x48, 0x65, 0x65, 0x24, 0x24, 0x02, 0x00],
3694        "Sigma Macro 90mm F2.8",
3695    ),
3696    (
3697        [0xE5, 0x54, 0x6A, 0x6A, 0x24, 0x24, 0x35, 0x02],
3698        "Sigma Macro 105mm F2.8 EX DG",
3699    ),
3700    (
3701        [0x97, 0x48, 0x6A, 0x6A, 0x24, 0x24, 0x4B, 0x0E],
3702        "Sigma Macro 105mm F2.8 EX DG OS HSM",
3703    ),
3704    (
3705        [0xBE, 0x54, 0x6A, 0x6A, 0x0C, 0x0C, 0x4B, 0x46],
3706        "Sigma 105mm F1.4 DG HSM | A",
3707    ),
3708    (
3709        [0x48, 0x48, 0x76, 0x76, 0x24, 0x24, 0x4B, 0x06],
3710        "Sigma APO Macro 150mm F2.8 EX DG HSM",
3711    ),
3712    (
3713        [0xF5, 0x48, 0x76, 0x76, 0x24, 0x24, 0x4B, 0x06],
3714        "Sigma APO Macro 150mm F2.8 EX DG HSM",
3715    ),
3716    (
3717        [0x99, 0x48, 0x76, 0x76, 0x24, 0x24, 0x4B, 0x0E],
3718        "Sigma APO Macro 150mm F2.8 EX DG OS HSM",
3719    ),
3720    (
3721        [0x48, 0x4C, 0x7C, 0x7C, 0x2C, 0x2C, 0x4B, 0x02],
3722        "Sigma APO Macro 180mm F3.5 EX DG HSM",
3723    ),
3724    (
3725        [0x48, 0x4C, 0x7D, 0x7D, 0x2C, 0x2C, 0x4B, 0x02],
3726        "Sigma APO Macro 180mm F3.5 EX DG HSM",
3727    ),
3728    (
3729        [0xF4, 0x4C, 0x7C, 0x7C, 0x2C, 0x2C, 0x4B, 0x02],
3730        "Sigma APO Macro 180mm F3.5 EX DG HSM",
3731    ),
3732    (
3733        [0x94, 0x48, 0x7C, 0x7C, 0x24, 0x24, 0x4B, 0x0E],
3734        "Sigma APO Macro 180mm F2.8 EX DG OS HSM",
3735    ),
3736    (
3737        [0x48, 0x54, 0x8E, 0x8E, 0x24, 0x24, 0x4B, 0x02],
3738        "Sigma APO 300mm F2.8 EX DG HSM",
3739    ),
3740    (
3741        [0xFB, 0x54, 0x8E, 0x8E, 0x24, 0x24, 0x4B, 0x02],
3742        "Sigma APO 300mm F2.8 EX DG HSM",
3743    ),
3744    (
3745        [0x26, 0x48, 0x8E, 0x8E, 0x30, 0x30, 0x1C, 0x02],
3746        "Sigma APO Tele Macro 300mm F4",
3747    ),
3748    (
3749        [0x02, 0x2F, 0x98, 0x98, 0x3D, 0x3D, 0x02, 0x00],
3750        "Sigma APO 400mm F5.6",
3751    ),
3752    (
3753        [0x26, 0x3C, 0x98, 0x98, 0x3C, 0x3C, 0x1C, 0x02],
3754        "Sigma APO Tele Macro 400mm F5.6",
3755    ),
3756    (
3757        [0x02, 0x37, 0xA0, 0xA0, 0x34, 0x34, 0x02, 0x00],
3758        "Sigma APO 500mm F4.5",
3759    ),
3760    (
3761        [0x48, 0x44, 0xA0, 0xA0, 0x34, 0x34, 0x4B, 0x02],
3762        "Sigma APO 500mm F4.5 EX HSM",
3763    ),
3764    (
3765        [0xF1, 0x44, 0xA0, 0xA0, 0x34, 0x34, 0x4B, 0x02],
3766        "Sigma APO 500mm F4.5 EX DG HSM",
3767    ),
3768    (
3769        [0x02, 0x34, 0xA0, 0xA0, 0x44, 0x44, 0x02, 0x00],
3770        "Sigma APO 500mm F7.2",
3771    ),
3772    (
3773        [0x02, 0x3C, 0xB0, 0xB0, 0x3C, 0x3C, 0x02, 0x00],
3774        "Sigma APO 800mm F5.6",
3775    ),
3776    (
3777        [0x48, 0x3C, 0xB0, 0xB0, 0x3C, 0x3C, 0x4B, 0x02],
3778        "Sigma APO 800mm F5.6 EX HSM",
3779    ),
3780    (
3781        [0x9E, 0x38, 0x11, 0x29, 0x34, 0x3C, 0x4B, 0x06],
3782        "Sigma 8-16mm F4.5-5.6 DC HSM",
3783    ),
3784    (
3785        [0xA1, 0x41, 0x19, 0x31, 0x2C, 0x2C, 0x4B, 0x06],
3786        "Sigma 10-20mm F3.5 EX DC HSM",
3787    ),
3788    (
3789        [0x48, 0x3C, 0x19, 0x31, 0x30, 0x3C, 0x4B, 0x06],
3790        "Sigma 10-20mm F4-5.6 EX DC HSM",
3791    ),
3792    (
3793        [0xF9, 0x3C, 0x19, 0x31, 0x30, 0x3C, 0x4B, 0x06],
3794        "Sigma 10-20mm F4-5.6 EX DC HSM",
3795    ),
3796    (
3797        [0x48, 0x38, 0x1F, 0x37, 0x34, 0x3C, 0x4B, 0x06],
3798        "Sigma 12-24mm F4.5-5.6 EX DG Aspherical HSM",
3799    ),
3800    (
3801        [0xF0, 0x38, 0x1F, 0x37, 0x34, 0x3C, 0x4B, 0x06],
3802        "Sigma 12-24mm F4.5-5.6 EX DG Aspherical HSM",
3803    ),
3804    (
3805        [0x96, 0x38, 0x1F, 0x37, 0x34, 0x3C, 0x4B, 0x06],
3806        "Sigma 12-24mm F4.5-5.6 II DG HSM",
3807    ),
3808    (
3809        [0xCA, 0x3C, 0x1F, 0x37, 0x30, 0x30, 0x4B, 0x46],
3810        "Sigma 12-24mm F4 DG HSM | A",
3811    ),
3812    (
3813        [0xC1, 0x48, 0x24, 0x37, 0x24, 0x24, 0x4B, 0x46],
3814        "Sigma 14-24mm F2.8 DG HSM | A",
3815    ),
3816    (
3817        [0x26, 0x40, 0x27, 0x3F, 0x2C, 0x34, 0x1C, 0x02],
3818        "Sigma 15-30mm F3.5-4.5 EX DG Aspherical DF",
3819    ),
3820    (
3821        [0x48, 0x48, 0x2B, 0x44, 0x24, 0x30, 0x4B, 0x06],
3822        "Sigma 17-35mm F2.8-4 EX DG  Aspherical HSM",
3823    ),
3824    (
3825        [0x26, 0x54, 0x2B, 0x44, 0x24, 0x30, 0x1C, 0x02],
3826        "Sigma 17-35mm F2.8-4 EX Aspherical",
3827    ),
3828    (
3829        [0x9D, 0x48, 0x2B, 0x50, 0x24, 0x24, 0x4B, 0x0E],
3830        "Sigma 17-50mm F2.8 EX DC OS HSM",
3831    ),
3832    (
3833        [0x8F, 0x48, 0x2B, 0x50, 0x24, 0x24, 0x4B, 0x0E],
3834        "Sigma 17-50mm F2.8 EX DC OS HSM",
3835    ),
3836    (
3837        [0x7A, 0x47, 0x2B, 0x5C, 0x24, 0x34, 0x4B, 0x06],
3838        "Sigma 17-70mm F2.8-4.5 DC Macro Asp. IF HSM",
3839    ),
3840    (
3841        [0x7A, 0x48, 0x2B, 0x5C, 0x24, 0x34, 0x4B, 0x06],
3842        "Sigma 17-70mm F2.8-4.5 DC Macro Asp. IF HSM",
3843    ),
3844    (
3845        [0x7F, 0x48, 0x2B, 0x5C, 0x24, 0x34, 0x1C, 0x06],
3846        "Sigma 17-70mm F2.8-4.5 DC Macro Asp. IF",
3847    ),
3848    (
3849        [0x8E, 0x3C, 0x2B, 0x5C, 0x24, 0x30, 0x4B, 0x0E],
3850        "Sigma 17-70mm F2.8-4 DC Macro OS HSM | C",
3851    ),
3852    (
3853        [0xA0, 0x48, 0x2A, 0x5C, 0x24, 0x30, 0x4B, 0x0E],
3854        "Sigma 17-70mm F2.8-4 DC Macro OS HSM",
3855    ),
3856    (
3857        [0x8B, 0x4C, 0x2D, 0x44, 0x14, 0x14, 0x4B, 0x06],
3858        "Sigma 18-35mm F1.8 DC HSM",
3859    ),
3860    (
3861        [0x26, 0x40, 0x2D, 0x44, 0x2B, 0x34, 0x1C, 0x02],
3862        "Sigma 18-35mm F3.5-4.5 Aspherical",
3863    ),
3864    (
3865        [0x26, 0x48, 0x2D, 0x50, 0x24, 0x24, 0x1C, 0x06],
3866        "Sigma 18-50mm F2.8 EX DC",
3867    ),
3868    (
3869        [0x7F, 0x48, 0x2D, 0x50, 0x24, 0x24, 0x1C, 0x06],
3870        "Sigma 18-50mm F2.8 EX DC Macro",
3871    ),
3872    (
3873        [0x7A, 0x48, 0x2D, 0x50, 0x24, 0x24, 0x4B, 0x06],
3874        "Sigma 18-50mm F2.8 EX DC Macro",
3875    ),
3876    (
3877        [0xF6, 0x48, 0x2D, 0x50, 0x24, 0x24, 0x4B, 0x06],
3878        "Sigma 18-50mm F2.8 EX DC Macro",
3879    ),
3880    (
3881        [0xA4, 0x47, 0x2D, 0x50, 0x24, 0x34, 0x4B, 0x0E],
3882        "Sigma 18-50mm F2.8-4.5 DC OS HSM",
3883    ),
3884    (
3885        [0x26, 0x40, 0x2D, 0x50, 0x2C, 0x3C, 0x1C, 0x06],
3886        "Sigma 18-50mm F3.5-5.6 DC",
3887    ),
3888    (
3889        [0x7A, 0x40, 0x2D, 0x50, 0x2C, 0x3C, 0x4B, 0x06],
3890        "Sigma 18-50mm F3.5-5.6 DC HSM",
3891    ),
3892    (
3893        [0x26, 0x40, 0x2D, 0x70, 0x2B, 0x3C, 0x1C, 0x06],
3894        "Sigma 18-125mm F3.5-5.6 DC",
3895    ),
3896    (
3897        [0xCD, 0x3D, 0x2D, 0x70, 0x2E, 0x3C, 0x4B, 0x0E],
3898        "Sigma 18-125mm F3.8-5.6 DC OS HSM",
3899    ),
3900    (
3901        [0x26, 0x40, 0x2D, 0x80, 0x2C, 0x40, 0x1C, 0x06],
3902        "Sigma 18-200mm F3.5-6.3 DC",
3903    ),
3904    (
3905        [0xFF, 0x40, 0x2D, 0x80, 0x2C, 0x40, 0x4B, 0x06],
3906        "Sigma 18-200mm F3.5-6.3 DC",
3907    ),
3908    (
3909        [0x7A, 0x40, 0x2D, 0x80, 0x2C, 0x40, 0x4B, 0x0E],
3910        "Sigma 18-200mm F3.5-6.3 DC OS HSM",
3911    ),
3912    (
3913        [0xED, 0x40, 0x2D, 0x80, 0x2C, 0x40, 0x4B, 0x0E],
3914        "Sigma 18-200mm F3.5-6.3 DC OS HSM",
3915    ),
3916    (
3917        [0x90, 0x40, 0x2D, 0x80, 0x2C, 0x40, 0x4B, 0x0E],
3918        "Sigma 18-200mm F3.5-6.3 II DC OS HSM",
3919    ),
3920    (
3921        [0x89, 0x30, 0x2D, 0x80, 0x2C, 0x40, 0x4B, 0x0E],
3922        "Sigma 18-200mm F3.5-6.3 DC Macro OS HS | C",
3923    ),
3924    (
3925        [0xA5, 0x40, 0x2D, 0x88, 0x2C, 0x40, 0x4B, 0x0E],
3926        "Sigma 18-250mm F3.5-6.3 DC OS HSM",
3927    ),
3928    (
3929        [0x92, 0x2C, 0x2D, 0x88, 0x2C, 0x40, 0x4B, 0x0E],
3930        "Sigma 18-250mm F3.5-6.3 DC Macro OS HSM",
3931    ),
3932    (
3933        [0x87, 0x2C, 0x2D, 0x8E, 0x2C, 0x40, 0x4B, 0x0E],
3934        "Sigma 18-300mm F3.5-6.3 DC Macro HSM",
3935    ),
3936    (
3937        [0x26, 0x48, 0x31, 0x49, 0x24, 0x24, 0x1C, 0x02],
3938        "Sigma 20-40mm F2.8",
3939    ),
3940    (
3941        [0x7B, 0x48, 0x37, 0x44, 0x18, 0x18, 0x4B, 0x06],
3942        "Sigma 24-35mm F2.0 DG HSM | A",
3943    ),
3944    (
3945        [0x02, 0x3A, 0x37, 0x50, 0x31, 0x3D, 0x02, 0x00],
3946        "Sigma 24-50mm F4-5.6 UC",
3947    ),
3948    (
3949        [0x26, 0x48, 0x37, 0x56, 0x24, 0x24, 0x1C, 0x02],
3950        "Sigma 24-60mm F2.8 EX DG",
3951    ),
3952    (
3953        [0xB6, 0x48, 0x37, 0x56, 0x24, 0x24, 0x1C, 0x02],
3954        "Sigma 24-60mm F2.8 EX DG",
3955    ),
3956    (
3957        [0xA6, 0x48, 0x37, 0x5C, 0x24, 0x24, 0x4B, 0x06],
3958        "Sigma 24-70mm F2.8 IF EX DG HSM",
3959    ),
3960    (
3961        [0xC9, 0x48, 0x37, 0x5C, 0x24, 0x24, 0x4B, 0x4E],
3962        "Sigma 24-70mm F2.8 DG OS HSM | A",
3963    ),
3964    (
3965        [0x26, 0x54, 0x37, 0x5C, 0x24, 0x24, 0x1C, 0x02],
3966        "Sigma 24-70mm F2.8 EX DG Macro",
3967    ),
3968    (
3969        [0x67, 0x54, 0x37, 0x5C, 0x24, 0x24, 0x1C, 0x02],
3970        "Sigma 24-70mm F2.8 EX DG Macro",
3971    ),
3972    (
3973        [0xE9, 0x54, 0x37, 0x5C, 0x24, 0x24, 0x1C, 0x02],
3974        "Sigma 24-70mm F2.8 EX DG Macro",
3975    ),
3976    (
3977        [0x26, 0x40, 0x37, 0x5C, 0x2C, 0x3C, 0x1C, 0x02],
3978        "Sigma 24-70mm F3.5-5.6 Aspherical HF",
3979    ),
3980    (
3981        [0x8A, 0x3C, 0x37, 0x6A, 0x30, 0x30, 0x4B, 0x0E],
3982        "Sigma 24-105mm F4 DG OS HSM",
3983    ),
3984    (
3985        [0x26, 0x54, 0x37, 0x73, 0x24, 0x34, 0x1C, 0x02],
3986        "Sigma 24-135mm F2.8-4.5",
3987    ),
3988    (
3989        [0x02, 0x46, 0x3C, 0x5C, 0x25, 0x25, 0x02, 0x00],
3990        "Sigma 28-70mm F2.8",
3991    ),
3992    (
3993        [0x26, 0x54, 0x3C, 0x5C, 0x24, 0x24, 0x1C, 0x02],
3994        "Sigma 28-70mm F2.8 EX",
3995    ),
3996    (
3997        [0x26, 0x48, 0x3C, 0x5C, 0x24, 0x24, 0x1C, 0x06],
3998        "Sigma 28-70mm F2.8 EX DG",
3999    ),
4000    (
4001        [0x79, 0x48, 0x3C, 0x5C, 0x24, 0x24, 0x1C, 0x06],
4002        "Sigma 28-70mm F2.8 EX DG",
4003    ),
4004    (
4005        [0x26, 0x48, 0x3C, 0x5C, 0x24, 0x30, 0x1C, 0x02],
4006        "Sigma 28-70mm F2.8-4 DG",
4007    ),
4008    (
4009        [0x02, 0x3F, 0x3C, 0x5C, 0x2D, 0x35, 0x02, 0x00],
4010        "Sigma 28-70mm F3.5-4.5 UC",
4011    ),
4012    (
4013        [0x26, 0x40, 0x3C, 0x60, 0x2C, 0x3C, 0x1C, 0x02],
4014        "Sigma 28-80mm F3.5-5.6 Mini Zoom Macro II Aspherical",
4015    ),
4016    (
4017        [0x26, 0x40, 0x3C, 0x65, 0x2C, 0x3C, 0x1C, 0x02],
4018        "Sigma 28-90mm F3.5-5.6 Macro",
4019    ),
4020    (
4021        [0x26, 0x48, 0x3C, 0x6A, 0x24, 0x30, 0x1C, 0x02],
4022        "Sigma 28-105mm F2.8-4 Aspherical",
4023    ),
4024    (
4025        [0x26, 0x3E, 0x3C, 0x6A, 0x2E, 0x3C, 0x1C, 0x02],
4026        "Sigma 28-105mm F3.8-5.6 UC-III Aspherical IF",
4027    ),
4028    (
4029        [0x26, 0x40, 0x3C, 0x80, 0x2C, 0x3C, 0x1C, 0x02],
4030        "Sigma 28-200mm F3.5-5.6 Compact Aspherical Hyperzoom Macro",
4031    ),
4032    (
4033        [0x26, 0x40, 0x3C, 0x80, 0x2B, 0x3C, 0x1C, 0x02],
4034        "Sigma 28-200mm F3.5-5.6 Compact Aspherical Hyperzoom Macro",
4035    ),
4036    (
4037        [0x26, 0x3D, 0x3C, 0x80, 0x2F, 0x3D, 0x1C, 0x02],
4038        "Sigma 28-300mm F3.8-5.6 Aspherical",
4039    ),
4040    (
4041        [0x26, 0x41, 0x3C, 0x8E, 0x2C, 0x40, 0x1C, 0x02],
4042        "Sigma 28-300mm F3.5-6.3 DG Macro",
4043    ),
4044    (
4045        [0xE6, 0x41, 0x3C, 0x8E, 0x2C, 0x40, 0x1C, 0x02],
4046        "Sigma 28-300mm F3.5-6.3 DG Macro",
4047    ),
4048    (
4049        [0x26, 0x40, 0x3C, 0x8E, 0x2C, 0x40, 0x1C, 0x02],
4050        "Sigma 28-300mm F3.5-6.3 Macro",
4051    ),
4052    (
4053        [0x02, 0x3B, 0x44, 0x61, 0x30, 0x3D, 0x02, 0x00],
4054        "Sigma 35-80mm F4-5.6",
4055    ),
4056    (
4057        [0x02, 0x40, 0x44, 0x73, 0x2B, 0x36, 0x02, 0x00],
4058        "Sigma 35-135mm F3.5-4.5 a",
4059    ),
4060    (
4061        [0xCC, 0x4C, 0x50, 0x68, 0x14, 0x14, 0x4B, 0x06],
4062        "Sigma 50-100mm F1.8 DC HSM | A",
4063    ),
4064    (
4065        [0x7A, 0x47, 0x50, 0x76, 0x24, 0x24, 0x4B, 0x06],
4066        "Sigma 50-150mm F2.8 EX APO DC HSM",
4067    ),
4068    (
4069        [0xFD, 0x47, 0x50, 0x76, 0x24, 0x24, 0x4B, 0x06],
4070        "Sigma 50-150mm F2.8 EX APO DC HSM II",
4071    ),
4072    (
4073        [0x98, 0x48, 0x50, 0x76, 0x24, 0x24, 0x4B, 0x0E],
4074        "Sigma 50-150mm F2.8 EX APO DC OS HSM",
4075    ),
4076    (
4077        [0x48, 0x3C, 0x50, 0xA0, 0x30, 0x40, 0x4B, 0x02],
4078        "Sigma 50-500mm F4-6.3 EX APO RF HSM",
4079    ),
4080    (
4081        [0x9F, 0x37, 0x50, 0xA0, 0x34, 0x40, 0x4B, 0x0E],
4082        "Sigma 50-500mm F4.5-6.3 DG OS HSM",
4083    ),
4084    (
4085        [0x26, 0x3C, 0x54, 0x80, 0x30, 0x3C, 0x1C, 0x06],
4086        "Sigma 55-200mm F4-5.6 DC",
4087    ),
4088    (
4089        [0x7A, 0x3B, 0x53, 0x80, 0x30, 0x3C, 0x4B, 0x06],
4090        "Sigma 55-200mm F4-5.6 DC HSM",
4091    ),
4092    (
4093        [0x48, 0x54, 0x5C, 0x80, 0x24, 0x24, 0x4B, 0x02],
4094        "Sigma 70-200mm F2.8 EX APO IF HSM",
4095    ),
4096    (
4097        [0x7A, 0x48, 0x5C, 0x80, 0x24, 0x24, 0x4B, 0x06],
4098        "Sigma 70-200mm F2.8 EX APO DG Macro HSM II",
4099    ),
4100    (
4101        [0xEE, 0x48, 0x5C, 0x80, 0x24, 0x24, 0x4B, 0x06],
4102        "Sigma 70-200mm F2.8 EX APO DG Macro HSM II",
4103    ),
4104    (
4105        [0x9C, 0x48, 0x5C, 0x80, 0x24, 0x24, 0x4B, 0x0E],
4106        "Sigma 70-200mm F2.8 EX DG OS HSM",
4107    ),
4108    (
4109        [0xBB, 0x48, 0x5C, 0x80, 0x24, 0x24, 0x4B, 0x4E],
4110        "Sigma 70-200mm F2.8 DG OS HSM | S",
4111    ),
4112    (
4113        [0x02, 0x46, 0x5C, 0x82, 0x25, 0x25, 0x02, 0x00],
4114        "Sigma 70-210mm F2.8 APO",
4115    ),
4116    (
4117        [0x02, 0x40, 0x5C, 0x82, 0x2C, 0x35, 0x02, 0x00],
4118        "Sigma APO 70-210mm F3.5-4.5",
4119    ),
4120    (
4121        [0x26, 0x3C, 0x5C, 0x82, 0x30, 0x3C, 0x1C, 0x02],
4122        "Sigma 70-210mm F4-5.6 UC-II",
4123    ),
4124    (
4125        [0x02, 0x3B, 0x5C, 0x82, 0x30, 0x3C, 0x02, 0x00],
4126        "Sigma Zoom-K 70-210mm F4-5.6",
4127    ),
4128    (
4129        [0x26, 0x3C, 0x5C, 0x8E, 0x30, 0x3C, 0x1C, 0x02],
4130        "Sigma 70-300mm F4-5.6 DG Macro",
4131    ),
4132    (
4133        [0x56, 0x3C, 0x5C, 0x8E, 0x30, 0x3C, 0x1C, 0x02],
4134        "Sigma 70-300mm F4-5.6 APO Macro Super II",
4135    ),
4136    (
4137        [0xE0, 0x3C, 0x5C, 0x8E, 0x30, 0x3C, 0x4B, 0x06],
4138        "Sigma 70-300mm F4-5.6 APO DG Macro HSM",
4139    ),
4140    (
4141        [0xA3, 0x3C, 0x5C, 0x8E, 0x30, 0x3C, 0x4B, 0x0E],
4142        "Sigma 70-300mm F4-5.6 DG OS",
4143    ),
4144    (
4145        [0x02, 0x37, 0x5E, 0x8E, 0x35, 0x3D, 0x02, 0x00],
4146        "Sigma 75-300mm F4.5-5.6 APO",
4147    ),
4148    (
4149        [0x02, 0x3A, 0x5E, 0x8E, 0x32, 0x3D, 0x02, 0x00],
4150        "Sigma 75-300mm F4.0-5.6",
4151    ),
4152    (
4153        [0x77, 0x44, 0x61, 0x98, 0x34, 0x3C, 0x7B, 0x0E],
4154        "Sigma 80-400mm F4.5-5.6 EX OS",
4155    ),
4156    (
4157        [0x77, 0x44, 0x60, 0x98, 0x34, 0x3C, 0x7B, 0x0E],
4158        "Sigma 80-400mm F4.5-5.6 APO DG D OS",
4159    ),
4160    (
4161        [0x48, 0x48, 0x68, 0x8E, 0x30, 0x30, 0x4B, 0x02],
4162        "Sigma APO 100-300mm F4 EX IF HSM",
4163    ),
4164    (
4165        [0xF3, 0x48, 0x68, 0x8E, 0x30, 0x30, 0x4B, 0x02],
4166        "Sigma APO 100-300mm F4 EX IF HSM",
4167    ),
4168    (
4169        [0x26, 0x45, 0x68, 0x8E, 0x34, 0x42, 0x1C, 0x02],
4170        "Sigma 100-300mm F4.5-6.7 DL",
4171    ),
4172    (
4173        [0x48, 0x54, 0x6F, 0x8E, 0x24, 0x24, 0x4B, 0x02],
4174        "Sigma APO 120-300mm F2.8 EX DG HSM",
4175    ),
4176    (
4177        [0x7A, 0x54, 0x6E, 0x8E, 0x24, 0x24, 0x4B, 0x02],
4178        "Sigma APO 120-300mm F2.8 EX DG HSM",
4179    ),
4180    (
4181        [0xFA, 0x54, 0x6E, 0x8E, 0x24, 0x24, 0x4B, 0x02],
4182        "Sigma APO 120-300mm F2.8 EX DG HSM",
4183    ),
4184    (
4185        [0xCF, 0x38, 0x6E, 0x98, 0x34, 0x3C, 0x4B, 0x0E],
4186        "Sigma APO 120-400mm F4.5-5.6 DG OS HSM",
4187    ),
4188    (
4189        [0xC3, 0x34, 0x68, 0x98, 0x38, 0x40, 0x4B, 0x4E],
4190        "Sigma 100-400mm F5-6.3 DG OS HSM | C",
4191    ),
4192    (
4193        [0x8D, 0x48, 0x6E, 0x8E, 0x24, 0x24, 0x4B, 0x0E],
4194        "Sigma 120-300mm F2.8 DG OS HSM Sports",
4195    ),
4196    (
4197        [0x26, 0x44, 0x73, 0x98, 0x34, 0x3C, 0x1C, 0x02],
4198        "Sigma 135-400mm F4.5-5.6 APO Aspherical",
4199    ),
4200    (
4201        [0xCE, 0x34, 0x76, 0xA0, 0x38, 0x40, 0x4B, 0x0E],
4202        "Sigma 150-500mm F5-6.3 DG OS APO HSM",
4203    ),
4204    (
4205        [0x81, 0x34, 0x76, 0xA6, 0x38, 0x40, 0x4B, 0x0E],
4206        "Sigma 150-600mm F5-6.3 DG OS HSM | S",
4207    ),
4208    (
4209        [0x82, 0x34, 0x76, 0xA6, 0x38, 0x40, 0x4B, 0x0E],
4210        "Sigma 150-600mm F5-6.3 DG OS HSM | C",
4211    ),
4212    (
4213        [0xC4, 0x4C, 0x73, 0x73, 0x14, 0x14, 0x4B, 0x46],
4214        "Sigma 135mm F1.8 DG HSM | A",
4215    ),
4216    (
4217        [0x26, 0x40, 0x7B, 0xA0, 0x34, 0x40, 0x1C, 0x02],
4218        "Sigma APO 170-500mm F5-6.3 Aspherical RF",
4219    ),
4220    (
4221        [0xA7, 0x49, 0x80, 0xA0, 0x24, 0x24, 0x4B, 0x06],
4222        "Sigma APO 200-500mm F2.8 EX DG",
4223    ),
4224    (
4225        [0x48, 0x3C, 0x8E, 0xB0, 0x3C, 0x3C, 0x4B, 0x02],
4226        "Sigma APO 300-800mm F5.6 EX DG HSM",
4227    ),
4228    (
4229        [0xD2, 0x3C, 0x8E, 0xB0, 0x3C, 0x3C, 0x4B, 0x02],
4230        "Sigma APO 300-800mm F5.6 EX DG HSM",
4231    ),
4232    (
4233        [0x00, 0x47, 0x25, 0x25, 0x24, 0x24, 0x00, 0x02],
4234        "Tamron SP AF 14mm f/2.8 Aspherical (IF) (69E)",
4235    ),
4236    (
4237        [0xC8, 0x54, 0x44, 0x44, 0x0D, 0x0D, 0xDF, 0x46],
4238        "Tamron SP 35mm f/1.4 Di USD (F045)",
4239    ),
4240    (
4241        [0xE8, 0x4C, 0x44, 0x44, 0x14, 0x14, 0xDF, 0x0E],
4242        "Tamron SP 35mm f/1.8 Di VC USD (F012)",
4243    ),
4244    (
4245        [0xE7, 0x4C, 0x4C, 0x4C, 0x14, 0x14, 0xDF, 0x0E],
4246        "Tamron SP 45mm f/1.8 Di VC USD (F013)",
4247    ),
4248    (
4249        [0xF4, 0x54, 0x56, 0x56, 0x18, 0x18, 0x84, 0x06],
4250        "Tamron SP AF 60mm f/2.0 Di II Macro 1:1 (G005)",
4251    ),
4252    (
4253        [0xE5, 0x4C, 0x62, 0x62, 0x14, 0x14, 0xC9, 0x4E],
4254        "Tamron SP 85mm f/1.8 Di VC USD (F016)",
4255    ),
4256    (
4257        [0x1E, 0x5D, 0x64, 0x64, 0x20, 0x20, 0x13, 0x00],
4258        "Tamron SP AF 90mm f/2.5 (52E)",
4259    ),
4260    (
4261        [0x20, 0x5A, 0x64, 0x64, 0x20, 0x20, 0x14, 0x00],
4262        "Tamron SP AF 90mm f/2.5 Macro (152E)",
4263    ),
4264    (
4265        [0x22, 0x53, 0x64, 0x64, 0x24, 0x24, 0xE0, 0x02],
4266        "Tamron SP AF 90mm f/2.8 Macro 1:1 (72E)",
4267    ),
4268    (
4269        [0x32, 0x53, 0x64, 0x64, 0x24, 0x24, 0x35, 0x02],
4270        "Tamron SP AF 90mm f/2.8 [Di] Macro 1:1 (172E/272E)",
4271    ),
4272    (
4273        [0xF8, 0x55, 0x64, 0x64, 0x24, 0x24, 0x84, 0x06],
4274        "Tamron SP AF 90mm f/2.8 Di Macro 1:1 (272NII)",
4275    ),
4276    (
4277        [0xF8, 0x54, 0x64, 0x64, 0x24, 0x24, 0xDF, 0x06],
4278        "Tamron SP AF 90mm f/2.8 Di Macro 1:1 (272NII)",
4279    ),
4280    (
4281        [0xFE, 0x54, 0x64, 0x64, 0x24, 0x24, 0xDF, 0x0E],
4282        "Tamron SP 90mm f/2.8 Di VC USD Macro 1:1 (F004)",
4283    ),
4284    (
4285        [0xE4, 0x54, 0x64, 0x64, 0x24, 0x24, 0xDF, 0x0E],
4286        "Tamron SP 90mm f/2.8 Di VC USD Macro 1:1 (F017)",
4287    ),
4288    (
4289        [0x00, 0x4C, 0x7C, 0x7C, 0x2C, 0x2C, 0x00, 0x02],
4290        "Tamron SP AF 180mm f/3.5 Di Model (B01)",
4291    ),
4292    (
4293        [0x21, 0x56, 0x8E, 0x8E, 0x24, 0x24, 0x14, 0x00],
4294        "Tamron SP AF 300mm f/2.8 LD-IF (60E)",
4295    ),
4296    (
4297        [0x27, 0x54, 0x8E, 0x8E, 0x24, 0x24, 0x1D, 0x02],
4298        "Tamron SP AF 300mm f/2.8 LD-IF (360E)",
4299    ),
4300    (
4301        [0xE1, 0x40, 0x19, 0x36, 0x2C, 0x35, 0xDF, 0x4E],
4302        "Tamron 10-24mm f/3.5-4.5 Di II VC HLD (B023)",
4303    ),
4304    (
4305        [0xE1, 0x40, 0x19, 0x36, 0x2C, 0x35, 0xDF, 0x0E],
4306        "Tamron 10-24mm f/3.5-4.5 Di II VC HLD (B023)",
4307    ),
4308    (
4309        [0xF6, 0x3F, 0x18, 0x37, 0x2C, 0x34, 0x84, 0x06],
4310        "Tamron SP AF 10-24mm f/3.5-4.5 Di II LD Aspherical (IF) (B001)",
4311    ),
4312    (
4313        [0xF6, 0x3F, 0x18, 0x37, 0x2C, 0x34, 0xDF, 0x06],
4314        "Tamron SP AF 10-24mm f/3.5-4.5 Di II LD Aspherical (IF) (B001)",
4315    ),
4316    (
4317        [0x00, 0x36, 0x1C, 0x2D, 0x34, 0x3C, 0x00, 0x06],
4318        "Tamron SP AF 11-18mm f/4.5-5.6 Di II LD Aspherical (IF) (A13)",
4319    ),
4320    (
4321        [0xE9, 0x48, 0x27, 0x3E, 0x24, 0x24, 0xDF, 0x0E],
4322        "Tamron SP 15-30mm f/2.8 Di VC USD (A012)",
4323    ),
4324    (
4325        [0xCA, 0x48, 0x27, 0x3E, 0x24, 0x24, 0xDF, 0x4E],
4326        "Tamron SP 15-30mm f/2.8 Di VC USD G2 (A041)",
4327    ),
4328    (
4329        [0xEA, 0x40, 0x29, 0x8E, 0x2C, 0x40, 0xDF, 0x0E],
4330        "Tamron 16-300mm f/3.5-6.3 Di II VC PZD (B016)",
4331    ),
4332    (
4333        [0x07, 0x46, 0x2B, 0x44, 0x24, 0x30, 0x03, 0x02],
4334        "Tamron SP AF 17-35mm f/2.8-4 Di LD Aspherical (IF) (A05)",
4335    ),
4336    (
4337        [0xCB, 0x3C, 0x2B, 0x44, 0x24, 0x31, 0xDF, 0x46],
4338        "Tamron 17-35mm f/2.8-4 Di OSD (A037)",
4339    ),
4340    (
4341        [0x00, 0x53, 0x2B, 0x50, 0x24, 0x24, 0x00, 0x06],
4342        "Tamron SP AF 17-50mm f/2.8 XR Di II LD Aspherical (IF) (A16)",
4343    ),
4344    (
4345        [0x7C, 0x54, 0x2B, 0x50, 0x24, 0x24, 0x00, 0x06],
4346        "Tamron SP AF 17-50mm f/2.8 XR Di II LD Aspherical (IF) (A16)",
4347    ),
4348    (
4349        [0x00, 0x54, 0x2B, 0x50, 0x24, 0x24, 0x00, 0x06],
4350        "Tamron SP AF 17-50mm f/2.8 XR Di II LD Aspherical (IF) (A16NII)",
4351    ),
4352    (
4353        [0xFB, 0x54, 0x2B, 0x50, 0x24, 0x24, 0x84, 0x06],
4354        "Tamron SP AF 17-50mm f/2.8 XR Di II LD Aspherical (IF) (A16NII)",
4355    ),
4356    (
4357        [0xF3, 0x54, 0x2B, 0x50, 0x24, 0x24, 0x84, 0x0E],
4358        "Tamron SP AF 17-50mm f/2.8 XR Di II VC LD Aspherical (IF) (B005)",
4359    ),
4360    (
4361        [0x00, 0x3F, 0x2D, 0x80, 0x2B, 0x40, 0x00, 0x06],
4362        "Tamron AF 18-200mm f/3.5-6.3 XR Di II LD Aspherical (IF) (A14)",
4363    ),
4364    (
4365        [0x00, 0x3F, 0x2D, 0x80, 0x2C, 0x40, 0x00, 0x06],
4366        "Tamron AF 18-200mm f/3.5-6.3 XR Di II LD Aspherical (IF) Macro (A14)",
4367    ),
4368    (
4369        [0xEC, 0x3E, 0x3C, 0x8E, 0x2C, 0x40, 0xDF, 0x0E],
4370        "Tamron 28-300mm f/3.5-6.3 Di VC PZD A010",
4371    ),
4372    (
4373        [0x00, 0x40, 0x2D, 0x80, 0x2C, 0x40, 0x00, 0x06],
4374        "Tamron AF 18-200mm f/3.5-6.3 XR Di II LD Aspherical (IF) Macro (A14NII)",
4375    ),
4376    (
4377        [0xFC, 0x40, 0x2D, 0x80, 0x2C, 0x40, 0xDF, 0x06],
4378        "Tamron AF 18-200mm f/3.5-6.3 XR Di II LD Aspherical (IF) Macro (A14NII)",
4379    ),
4380    (
4381        [0xE6, 0x40, 0x2D, 0x80, 0x2C, 0x40, 0xDF, 0x0E],
4382        "Tamron 18-200mm f/3.5-6.3 Di II VC (B018)",
4383    ),
4384    (
4385        [0x00, 0x40, 0x2D, 0x88, 0x2C, 0x40, 0x62, 0x06],
4386        "Tamron AF 18-250mm f/3.5-6.3 Di II LD Aspherical (IF) Macro (A18)",
4387    ),
4388    (
4389        [0x00, 0x40, 0x2D, 0x88, 0x2C, 0x40, 0x00, 0x06],
4390        "Tamron AF 18-250mm f/3.5-6.3 Di II LD Aspherical (IF) Macro (A18NII)",
4391    ),
4392    (
4393        [0xF5, 0x40, 0x2C, 0x8A, 0x2C, 0x40, 0x40, 0x0E],
4394        "Tamron AF 18-270mm f/3.5-6.3 Di II VC LD Aspherical (IF) Macro (B003)",
4395    ),
4396    (
4397        [0xF0, 0x3F, 0x2D, 0x8A, 0x2C, 0x40, 0xDF, 0x0E],
4398        "Tamron AF 18-270mm f/3.5-6.3 Di II VC PZD (B008)",
4399    ),
4400    (
4401        [0xE0, 0x40, 0x2D, 0x98, 0x2C, 0x41, 0xDF, 0x0E],
4402        "Tamron 18-400mm f/3.5-6.3 Di II VC HLD (B028)",
4403    ),
4404    (
4405        [0xE0, 0x40, 0x2D, 0x98, 0x2C, 0x41, 0xDF, 0x4E],
4406        "Tamron 18-400mm f/3.5-6.3 Di II VC HLD (B028)",
4407    ),
4408    (
4409        [0x07, 0x40, 0x2F, 0x44, 0x2C, 0x34, 0x03, 0x02],
4410        "Tamron AF 19-35mm f/3.5-4.5 (A10)",
4411    ),
4412    (
4413        [0x00, 0x49, 0x30, 0x48, 0x22, 0x2B, 0x00, 0x02],
4414        "Tamron SP AF 20-40mm f/2.7-3.5 (166D)",
4415    ),
4416    (
4417        [0x0E, 0x4A, 0x31, 0x48, 0x23, 0x2D, 0x0E, 0x02],
4418        "Tamron SP AF 20-40mm f/2.7-3.5 (166D)",
4419    ),
4420    (
4421        [0xFE, 0x48, 0x37, 0x5C, 0x24, 0x24, 0xDF, 0x0E],
4422        "Tamron SP 24-70mm f/2.8 Di VC USD (A007)",
4423    ),
4424    (
4425        [0xCE, 0x47, 0x37, 0x5C, 0x25, 0x25, 0xDF, 0x4E],
4426        "Tamron SP 24-70mm f/2.8 Di VC USD G2 (A032)",
4427    ),
4428    (
4429        [0xCE, 0x00, 0x37, 0x5C, 0x25, 0x25, 0xDF, 0x4E],
4430        "Tamron SP 24-70mm f/2.8 Di VC USD G2 (A032)",
4431    ),
4432    (
4433        [0x45, 0x41, 0x37, 0x72, 0x2C, 0x3C, 0x48, 0x02],
4434        "Tamron SP AF 24-135mm f/3.5-5.6 AD Aspherical (IF) Macro (190D)",
4435    ),
4436    (
4437        [0x33, 0x54, 0x3C, 0x5E, 0x24, 0x24, 0x62, 0x02],
4438        "Tamron SP AF 28-75mm f/2.8 XR Di LD Aspherical (IF) Macro (A09)",
4439    ),
4440    (
4441        [0xFA, 0x54, 0x3C, 0x5E, 0x24, 0x24, 0x84, 0x06],
4442        "Tamron SP AF 28-75mm f/2.8 XR Di LD Aspherical (IF) Macro (A09NII)",
4443    ),
4444    (
4445        [0xFA, 0x54, 0x3C, 0x5E, 0x24, 0x24, 0xDF, 0x06],
4446        "Tamron SP AF 28-75mm f/2.8 XR Di LD Aspherical (IF) Macro (A09NII)",
4447    ),
4448    (
4449        [0x10, 0x3D, 0x3C, 0x60, 0x2C, 0x3C, 0xD2, 0x02],
4450        "Tamron AF 28-80mm f/3.5-5.6 Aspherical (177D)",
4451    ),
4452    (
4453        [0x45, 0x3D, 0x3C, 0x60, 0x2C, 0x3C, 0x48, 0x02],
4454        "Tamron AF 28-80mm f/3.5-5.6 Aspherical (177D)",
4455    ),
4456    (
4457        [0x00, 0x48, 0x3C, 0x6A, 0x24, 0x24, 0x00, 0x02],
4458        "Tamron SP AF 28-105mm f/2.8 LD Aspherical IF (176D)",
4459    ),
4460    (
4461        [0x4D, 0x3E, 0x3C, 0x80, 0x2E, 0x3C, 0x62, 0x02],
4462        "Tamron AF 28-200mm f/3.8-5.6 XR Aspherical (IF) Macro (A03N)",
4463    ),
4464    (
4465        [0x0B, 0x3E, 0x3D, 0x7F, 0x2F, 0x3D, 0x0E, 0x00],
4466        "Tamron AF 28-200mm f/3.8-5.6 (71D)",
4467    ),
4468    (
4469        [0x0B, 0x3E, 0x3D, 0x7F, 0x2F, 0x3D, 0x0E, 0x02],
4470        "Tamron AF 28-200mm f/3.8-5.6D (171D)",
4471    ),
4472    (
4473        [0x12, 0x3D, 0x3C, 0x80, 0x2E, 0x3C, 0xDF, 0x02],
4474        "Tamron AF 28-200mm f/3.8-5.6 AF Aspherical LD (IF) (271D)",
4475    ),
4476    (
4477        [0x4D, 0x41, 0x3C, 0x8E, 0x2B, 0x40, 0x62, 0x02],
4478        "Tamron AF 28-300mm f/3.5-6.3 XR Di LD Aspherical (IF) (A061)",
4479    ),
4480    (
4481        [0x4D, 0x41, 0x3C, 0x8E, 0x2C, 0x40, 0x62, 0x02],
4482        "Tamron AF 28-300mm f/3.5-6.3 XR LD Aspherical (IF) (185D)",
4483    ),
4484    (
4485        [0xF9, 0x40, 0x3C, 0x8E, 0x2C, 0x40, 0x40, 0x0E],
4486        "Tamron AF 28-300mm f/3.5-6.3 XR Di VC LD Aspherical (IF) Macro (A20)",
4487    ),
4488    (
4489        [0xC9, 0x3C, 0x44, 0x76, 0x25, 0x31, 0xDF, 0x4E],
4490        "Tamron 35-150mm f/2.8-4 Di VC OSD (A043)",
4491    ),
4492    (
4493        [0x00, 0x47, 0x53, 0x80, 0x30, 0x3C, 0x00, 0x06],
4494        "Tamron AF 55-200mm f/4-5.6 Di II LD (A15)",
4495    ),
4496    (
4497        [0xF7, 0x53, 0x5C, 0x80, 0x24, 0x24, 0x84, 0x06],
4498        "Tamron SP AF 70-200mm f/2.8 Di LD (IF) Macro (A001)",
4499    ),
4500    (
4501        [0xFE, 0x53, 0x5C, 0x80, 0x24, 0x24, 0x84, 0x06],
4502        "Tamron SP AF 70-200mm f/2.8 Di LD (IF) Macro (A001)",
4503    ),
4504    (
4505        [0xF7, 0x53, 0x5C, 0x80, 0x24, 0x24, 0x40, 0x06],
4506        "Tamron SP AF 70-200mm f/2.8 Di LD (IF) Macro (A001)",
4507    ),
4508    (
4509        [0xFE, 0x54, 0x5C, 0x80, 0x24, 0x24, 0xDF, 0x0E],
4510        "Tamron SP 70-200mm f/2.8 Di VC USD (A009)",
4511    ),
4512    (
4513        [0xE2, 0x47, 0x5C, 0x80, 0x24, 0x24, 0xDF, 0x4E],
4514        "Tamron SP 70-200mm f/2.8 Di VC USD G2 (A025)",
4515    ),
4516    (
4517        [0x69, 0x48, 0x5C, 0x8E, 0x30, 0x3C, 0x6F, 0x02],
4518        "Tamron AF 70-300mm f/4-5.6 LD Macro 1:2 (572D/772D)",
4519    ),
4520    (
4521        [0x69, 0x47, 0x5C, 0x8E, 0x30, 0x3C, 0x00, 0x02],
4522        "Tamron AF 70-300mm f/4-5.6 Di LD Macro 1:2 (A17N)",
4523    ),
4524    (
4525        [0x00, 0x48, 0x5C, 0x8E, 0x30, 0x3C, 0x00, 0x06],
4526        "Tamron AF 70-300mm f/4-5.6 Di LD Macro 1:2 (A17NII)",
4527    ),
4528    (
4529        [0xF1, 0x47, 0x5C, 0x8E, 0x30, 0x3C, 0xDF, 0x0E],
4530        "Tamron SP 70-300mm f/4-5.6 Di VC USD (A005)",
4531    ),
4532    (
4533        [0xCF, 0x47, 0x5C, 0x8E, 0x31, 0x3D, 0xDF, 0x0E],
4534        "Tamron SP 70-300mm f/4-5.6 Di VC USD (A030)",
4535    ),
4536    (
4537        [0xCC, 0x44, 0x68, 0x98, 0x34, 0x41, 0xDF, 0x0E],
4538        "Tamron 100-400mm f/4.5-6.3 Di VC USD",
4539    ),
4540    (
4541        [0xEB, 0x40, 0x76, 0xA6, 0x38, 0x40, 0xDF, 0x0E],
4542        "Tamron SP AF 150-600mm f/5-6.3 VC USD (A011)",
4543    ),
4544    (
4545        [0xE3, 0x40, 0x76, 0xA6, 0x38, 0x40, 0xDF, 0x4E],
4546        "Tamron SP 150-600mm f/5-6.3 Di VC USD G2",
4547    ),
4548    (
4549        [0xE3, 0x40, 0x76, 0xA6, 0x38, 0x40, 0xDF, 0x0E],
4550        "Tamron SP 150-600mm f/5-6.3 Di VC USD G2 (A022)",
4551    ),
4552    (
4553        [0x20, 0x3C, 0x80, 0x98, 0x3D, 0x3D, 0x1E, 0x02],
4554        "Tamron AF 200-400mm f/5.6 LD IF (75D)",
4555    ),
4556    (
4557        [0x00, 0x3E, 0x80, 0xA0, 0x38, 0x3F, 0x00, 0x02],
4558        "Tamron SP AF 200-500mm f/5-6.3 Di LD (IF) (A08)",
4559    ),
4560    (
4561        [0x00, 0x3F, 0x80, 0xA0, 0x38, 0x3F, 0x00, 0x02],
4562        "Tamron SP AF 200-500mm f/5-6.3 Di (A08)",
4563    ),
4564    (
4565        [0x00, 0x40, 0x2B, 0x2B, 0x2C, 0x2C, 0x00, 0x02],
4566        "Tokina AT-X 17 AF PRO (AF 17mm f/3.5)",
4567    ),
4568    (
4569        [0x00, 0x47, 0x44, 0x44, 0x24, 0x24, 0x00, 0x06],
4570        "Tokina AT-X M35 PRO DX (AF 35mm f/2.8 Macro)",
4571    ),
4572    (
4573        [0x8D, 0x54, 0x68, 0x68, 0x24, 0x24, 0x87, 0x02],
4574        "Tokina AT-X PRO 100mm F2.8 D Macro",
4575    ),
4576    (
4577        [0x00, 0x54, 0x68, 0x68, 0x24, 0x24, 0x00, 0x02],
4578        "Tokina AT-X M100 AF PRO D (AF 100mm f/2.8 Macro)",
4579    ),
4580    (
4581        [0x27, 0x48, 0x8E, 0x8E, 0x30, 0x30, 0x1D, 0x02],
4582        "Tokina AT-X 304 AF (AF 300mm f/4.0)",
4583    ),
4584    (
4585        [0x00, 0x54, 0x8E, 0x8E, 0x24, 0x24, 0x00, 0x02],
4586        "Tokina AT-X 300 AF PRO (AF 300mm f/2.8)",
4587    ),
4588    (
4589        [0x12, 0x3B, 0x98, 0x98, 0x3D, 0x3D, 0x09, 0x00],
4590        "Tokina AT-X 400 AF SD (AF 400mm f/5.6)",
4591    ),
4592    (
4593        [0x00, 0x40, 0x18, 0x2B, 0x2C, 0x34, 0x00, 0x06],
4594        "Tokina AT-X 107 AF DX Fisheye (AF 10-17mm f/3.5-4.5)",
4595    ),
4596    (
4597        [0x00, 0x48, 0x1C, 0x29, 0x24, 0x24, 0x00, 0x06],
4598        "Tokina AT-X 116 PRO DX (AF 11-16mm f/2.8)",
4599    ),
4600    (
4601        [0x7A, 0x48, 0x1C, 0x29, 0x24, 0x24, 0x7E, 0x06],
4602        "Tokina AT-X 116 PRO DX II (AF 11-16mm f/2.8)",
4603    ),
4604    (
4605        [0x80, 0x48, 0x1C, 0x29, 0x24, 0x24, 0x7A, 0x06],
4606        "Tokina atx-i 11-16mm F2.8 CF",
4607    ),
4608    (
4609        [0x7A, 0x48, 0x1C, 0x30, 0x24, 0x24, 0x7E, 0x06],
4610        "Tokina AT-X 11-20 F2.8 PRO DX (AF 11-20mm f/2.8)",
4611    ),
4612    (
4613        [0x8B, 0x48, 0x1C, 0x30, 0x24, 0x24, 0x85, 0x06],
4614        "Tokina AT-X 11-20 F2.8 PRO DX (AF 11-20mm f/2.8)",
4615    ),
4616    (
4617        [0x00, 0x3C, 0x1F, 0x37, 0x30, 0x30, 0x00, 0x06],
4618        "Tokina AT-X 124 AF PRO DX (AF 12-24mm f/4)",
4619    ),
4620    (
4621        [0x7A, 0x3C, 0x1F, 0x3C, 0x30, 0x30, 0x7E, 0x06],
4622        "Tokina AT-X 12-28 PRO DX (AF 12-28mm f/4)",
4623    ),
4624    (
4625        [0x00, 0x48, 0x29, 0x3C, 0x24, 0x24, 0x00, 0x06],
4626        "Tokina AT-X 16-28 AF PRO FX (AF 16-28mm f/2.8)",
4627    ),
4628    (
4629        [0x00, 0x48, 0x29, 0x50, 0x24, 0x24, 0x00, 0x06],
4630        "Tokina AT-X 165 PRO DX (AF 16-50mm f/2.8)",
4631    ),
4632    (
4633        [0x00, 0x40, 0x2A, 0x72, 0x2C, 0x3C, 0x00, 0x06],
4634        "Tokina AT-X 16.5-135 DX (AF 16.5-135mm F3.5-5.6)",
4635    ),
4636    (
4637        [0x00, 0x3C, 0x2B, 0x44, 0x30, 0x30, 0x00, 0x06],
4638        "Tokina AT-X 17-35 F4 PRO FX (AF 17-35mm f/4)",
4639    ),
4640    (
4641        [0x00, 0x48, 0x37, 0x5C, 0x24, 0x24, 0x00, 0x06],
4642        "Tokina AT-X 24-70 F2.8 PRO FX (AF 24-70mm f/2.8)",
4643    ),
4644    (
4645        [0x00, 0x40, 0x37, 0x80, 0x2C, 0x3C, 0x00, 0x02],
4646        "Tokina AT-X 242 AF (AF 24-200mm f/3.5-5.6)",
4647    ),
4648    (
4649        [0x07, 0x48, 0x3C, 0x5C, 0x24, 0x24, 0x03, 0x00],
4650        "Tokina AT-X 287 AF (AF 28-70mm f/2.8)",
4651    ),
4652    (
4653        [0x07, 0x47, 0x3C, 0x5C, 0x25, 0x35, 0x03, 0x00],
4654        "Tokina AF 287 SD (AF 28-70mm f/2.8-4.5)",
4655    ),
4656    (
4657        [0x07, 0x40, 0x3C, 0x5C, 0x2C, 0x35, 0x03, 0x00],
4658        "Tokina AF 270 II (AF 28-70mm f/3.5-4.5)",
4659    ),
4660    (
4661        [0x00, 0x48, 0x3C, 0x60, 0x24, 0x24, 0x00, 0x02],
4662        "Tokina AT-X 280 AF PRO (AF 28-80mm f/2.8)",
4663    ),
4664    (
4665        [0x25, 0x44, 0x44, 0x8E, 0x34, 0x42, 0x1B, 0x02],
4666        "Tokina AF 353 (AF 35-300mm f/4.5-6.7)",
4667    ),
4668    (
4669        [0x00, 0x48, 0x50, 0x72, 0x24, 0x24, 0x00, 0x06],
4670        "Tokina AT-X 535 PRO DX (AF 50-135mm f/2.8)",
4671    ),
4672    (
4673        [0x00, 0x3C, 0x5C, 0x80, 0x30, 0x30, 0x00, 0x0E],
4674        "Tokina AT-X 70-200 F4 FX VCM-S (AF 70-200mm f/4)",
4675    ),
4676    (
4677        [0x00, 0x48, 0x5C, 0x80, 0x30, 0x30, 0x00, 0x0E],
4678        "Tokina AT-X 70-200 F4 FX VCM-S (AF 70-200mm f/4)",
4679    ),
4680    (
4681        [0x12, 0x44, 0x5E, 0x8E, 0x34, 0x3C, 0x09, 0x00],
4682        "Tokina AF 730 (AF 75-300mm F4.5-5.6)",
4683    ),
4684    (
4685        [0x14, 0x54, 0x60, 0x80, 0x24, 0x24, 0x0B, 0x00],
4686        "Tokina AT-X 828 AF (AF 80-200mm f/2.8)",
4687    ),
4688    (
4689        [0x24, 0x54, 0x60, 0x80, 0x24, 0x24, 0x1A, 0x02],
4690        "Tokina AT-X 828 AF PRO (AF 80-200mm f/2.8)",
4691    ),
4692    (
4693        [0x24, 0x44, 0x60, 0x98, 0x34, 0x3C, 0x1A, 0x02],
4694        "Tokina AT-X 840 AF-II (AF 80-400mm f/4.5-5.6)",
4695    ),
4696    (
4697        [0x00, 0x44, 0x60, 0x98, 0x34, 0x3C, 0x00, 0x02],
4698        "Tokina AT-X 840 D (AF 80-400mm f/4.5-5.6)",
4699    ),
4700    (
4701        [0x14, 0x48, 0x68, 0x8E, 0x30, 0x30, 0x0B, 0x00],
4702        "Tokina AT-X 340 AF (AF 100-300mm f/4)",
4703    ),
4704    (
4705        [0x8C, 0x48, 0x29, 0x3C, 0x24, 0x24, 0x86, 0x06],
4706        "Tokina opera 16-28mm F2.8 FF",
4707    ),
4708    (
4709        [0x06, 0x3F, 0x68, 0x68, 0x2C, 0x2C, 0x06, 0x00],
4710        "Cosina AF 100mm F3.5 Macro",
4711    ),
4712    (
4713        [0x07, 0x36, 0x3D, 0x5F, 0x2C, 0x3C, 0x03, 0x00],
4714        "Cosina AF Zoom 28-80mm F3.5-5.6 MC Macro",
4715    ),
4716    (
4717        [0x07, 0x46, 0x3D, 0x6A, 0x25, 0x2F, 0x03, 0x00],
4718        "Cosina AF Zoom 28-105mm F2.8-3.8 MC",
4719    ),
4720    (
4721        [0x12, 0x36, 0x5C, 0x81, 0x35, 0x3D, 0x09, 0x00],
4722        "Cosina AF Zoom 70-210mm F4.5-5.6 MC Macro",
4723    ),
4724    (
4725        [0x12, 0x39, 0x5C, 0x8E, 0x34, 0x3D, 0x08, 0x02],
4726        "Cosina AF Zoom 70-300mm F4.5-5.6 MC Macro",
4727    ),
4728    (
4729        [0x12, 0x3B, 0x68, 0x8D, 0x3D, 0x43, 0x09, 0x02],
4730        "Cosina AF Zoom 100-300mm F5.6-6.7 MC Macro",
4731    ),
4732    (
4733        [0x12, 0x38, 0x69, 0x97, 0x35, 0x42, 0x09, 0x02],
4734        "Promaster Spectrum 7 100-400mm F4.5-6.7",
4735    ),
4736    (
4737        [0x00, 0x40, 0x31, 0x31, 0x2C, 0x2C, 0x00, 0x00],
4738        "Voigtlander Color Skopar 20mm F3.5 SLII Aspherical",
4739    ),
4740    (
4741        [0x00, 0x48, 0x3C, 0x3C, 0x24, 0x24, 0x00, 0x00],
4742        "Voigtlander Color Skopar 28mm F2.8 SL II",
4743    ),
4744    (
4745        [0x00, 0x54, 0x48, 0x48, 0x18, 0x18, 0x00, 0x00],
4746        "Voigtlander Ultron 40mm F2 SLII Aspherical",
4747    ),
4748    (
4749        [0x00, 0x54, 0x55, 0x55, 0x0C, 0x0C, 0x00, 0x00],
4750        "Voigtlander Nokton 58mm F1.4 SLII",
4751    ),
4752    (
4753        [0x00, 0x40, 0x64, 0x64, 0x2C, 0x2C, 0x00, 0x00],
4754        "Voigtlander APO-Lanthar 90mm F3.5 SLII Close Focus",
4755    ),
4756    (
4757        [0x71, 0x48, 0x64, 0x64, 0x24, 0x24, 0x00, 0x00],
4758        "Voigtlander APO-Skopar 90mm F2.8 SL IIs",
4759    ),
4760    (
4761        [0xFD, 0x00, 0x50, 0x50, 0x18, 0x18, 0xDF, 0x00],
4762        "Voigtlander APO-Lanthar 50mm F2 Aspherical",
4763    ),
4764    (
4765        [0xFD, 0x00, 0x44, 0x44, 0x18, 0x18, 0xDF, 0x00],
4766        "Voigtlander APO-Lanthar 35mm F2",
4767    ),
4768    (
4769        [0xFD, 0x00, 0x59, 0x59, 0x18, 0x18, 0xDF, 0x00],
4770        "Voigtlander Macro APO-Lanthar 65mm F2",
4771    ),
4772    (
4773        [0xFD, 0x00, 0x48, 0x48, 0x07, 0x07, 0xDF, 0x00],
4774        "Voigtlander Nokton 40mm F1.2 Aspherical",
4775    ),
4776    (
4777        [0xFD, 0x00, 0x3C, 0x3C, 0x18, 0x18, 0xDF, 0x00],
4778        "Voigtlander APO-Lanthar 28mm F2 Aspherical",
4779    ),
4780    (
4781        [0x00, 0x40, 0x2D, 0x2D, 0x2C, 0x2C, 0x00, 0x00],
4782        "Carl Zeiss Distagon T* 3.5/18 ZF.2",
4783    ),
4784    (
4785        [0x00, 0x48, 0x27, 0x27, 0x24, 0x24, 0x00, 0x00],
4786        "Carl Zeiss Distagon T* 2.8/15 ZF.2",
4787    ),
4788    (
4789        [0x00, 0x48, 0x32, 0x32, 0x24, 0x24, 0x00, 0x00],
4790        "Carl Zeiss Distagon T* 2.8/21 ZF.2",
4791    ),
4792    (
4793        [0x00, 0x54, 0x38, 0x38, 0x18, 0x18, 0x00, 0x00],
4794        "Carl Zeiss Distagon T* 2/25 ZF.2",
4795    ),
4796    (
4797        [0x00, 0x54, 0x3C, 0x3C, 0x18, 0x18, 0x00, 0x00],
4798        "Carl Zeiss Distagon T* 2/28 ZF.2",
4799    ),
4800    (
4801        [0x00, 0x54, 0x44, 0x44, 0x0C, 0x0C, 0x00, 0x00],
4802        "Carl Zeiss Distagon T* 1.4/35 ZF.2",
4803    ),
4804    (
4805        [0x00, 0x54, 0x44, 0x44, 0x18, 0x18, 0x00, 0x00],
4806        "Carl Zeiss Distagon T* 2/35 ZF.2",
4807    ),
4808    (
4809        [0x00, 0x54, 0x50, 0x50, 0x0C, 0x0C, 0x00, 0x00],
4810        "Carl Zeiss Planar T* 1.4/50 ZF.2",
4811    ),
4812    (
4813        [0x00, 0x54, 0x50, 0x50, 0x18, 0x18, 0x00, 0x00],
4814        "Carl Zeiss Makro-Planar T* 2/50 ZF.2",
4815    ),
4816    (
4817        [0x00, 0x54, 0x62, 0x62, 0x0C, 0x0C, 0x00, 0x00],
4818        "Carl Zeiss Planar T* 1.4/85 ZF.2",
4819    ),
4820    (
4821        [0x00, 0x54, 0x68, 0x68, 0x18, 0x18, 0x00, 0x00],
4822        "Carl Zeiss Makro-Planar T* 2/100 ZF.2",
4823    ),
4824    (
4825        [0x00, 0x54, 0x72, 0x72, 0x18, 0x18, 0x00, 0x00],
4826        "Carl Zeiss Apo Sonnar T* 2/135 ZF.2",
4827    ),
4828    (
4829        [0x02, 0x54, 0x3C, 0x3C, 0x0C, 0x0C, 0x00, 0x00],
4830        "Zeiss Otus 1.4/28 ZF.2",
4831    ),
4832    (
4833        [0x00, 0x54, 0x53, 0x53, 0x0C, 0x0C, 0x00, 0x00],
4834        "Zeiss Otus 1.4/55",
4835    ),
4836    (
4837        [0x01, 0x54, 0x62, 0x62, 0x0C, 0x0C, 0x00, 0x00],
4838        "Zeiss Otus 1.4/85",
4839    ),
4840    (
4841        [0x03, 0x54, 0x68, 0x68, 0x0C, 0x0C, 0x00, 0x00],
4842        "Zeiss Otus 1.4/100",
4843    ),
4844    (
4845        [0x52, 0x54, 0x44, 0x44, 0x18, 0x18, 0x00, 0x00],
4846        "Zeiss Milvus 35mm f/2",
4847    ),
4848    (
4849        [0x53, 0x54, 0x50, 0x50, 0x0C, 0x0C, 0x00, 0x00],
4850        "Zeiss Milvus 50mm f/1.4",
4851    ),
4852    (
4853        [0x54, 0x54, 0x50, 0x50, 0x18, 0x18, 0x00, 0x00],
4854        "Zeiss Milvus 50mm f/2 Macro",
4855    ),
4856    (
4857        [0x55, 0x54, 0x62, 0x62, 0x0C, 0x0C, 0x00, 0x00],
4858        "Zeiss Milvus 85mm f/1.4",
4859    ),
4860    (
4861        [0x56, 0x54, 0x68, 0x68, 0x18, 0x18, 0x00, 0x00],
4862        "Zeiss Milvus 100mm f/2 Macro",
4863    ),
4864    (
4865        [0x00, 0x54, 0x56, 0x56, 0x30, 0x30, 0x00, 0x00],
4866        "Coastal Optical Systems 60mm 1:4 UV-VIS-IR Macro Apo",
4867    ),
4868    (
4869        [0xBF, 0x4E, 0x26, 0x26, 0x1E, 0x1E, 0x01, 0x04],
4870        "Irix 15mm f/2.4 Firefly",
4871    ),
4872    (
4873        [0xBF, 0x3C, 0x1B, 0x1B, 0x30, 0x30, 0x01, 0x04],
4874        "Irix 11mm f/4 Firefly",
4875    ),
4876    (
4877        [0x4A, 0x40, 0x11, 0x11, 0x2C, 0x0C, 0x4D, 0x02],
4878        "Samyang 8mm f/3.5 Fish-Eye CS",
4879    ),
4880    (
4881        [0x4A, 0x48, 0x1E, 0x1E, 0x24, 0x0C, 0x4D, 0x02],
4882        "Samyang 12mm f/2.8 ED AS NCS Fish-Eye",
4883    ),
4884    (
4885        [0x4A, 0x4C, 0x24, 0x24, 0x1E, 0x6C, 0x4D, 0x06],
4886        "Samyang 14mm f/2.4 Premium",
4887    ),
4888    (
4889        [0x4A, 0x54, 0x29, 0x29, 0x18, 0x0C, 0x4D, 0x02],
4890        "Samyang 16mm f/2.0 ED AS UMC CS",
4891    ),
4892    (
4893        [0x4A, 0x60, 0x36, 0x36, 0x0C, 0x0C, 0x4D, 0x02],
4894        "Samyang 24mm f/1.4 ED AS UMC",
4895    ),
4896    (
4897        [0x4A, 0x60, 0x44, 0x44, 0x0C, 0x0C, 0x4D, 0x02],
4898        "Samyang 35mm f/1.4 AS UMC",
4899    ),
4900    (
4901        [0x4A, 0x60, 0x62, 0x62, 0x0C, 0x0C, 0x4D, 0x02],
4902        "Samyang AE 85mm f/1.4 AS IF UMC",
4903    ),
4904    (
4905        [0x9A, 0x4C, 0x50, 0x50, 0x14, 0x14, 0x9C, 0x06],
4906        "Yongnuo YN50mm F1.8N",
4907    ),
4908    (
4909        [0x9F, 0x48, 0x48, 0x48, 0x24, 0x24, 0xA1, 0x06],
4910        "Yongnuo YN40mm F2.8N",
4911    ),
4912    (
4913        [0x9F, 0x54, 0x68, 0x68, 0x18, 0x18, 0xA2, 0x06],
4914        "Yongnuo YN100mm F2N",
4915    ),
4916    (
4917        [0x9F, 0x4C, 0x44, 0x44, 0x18, 0x18, 0xA1, 0x06],
4918        "Yongnuo YN35mm F2",
4919    ),
4920    (
4921        [0x9F, 0x4D, 0x50, 0x50, 0x14, 0x14, 0xA0, 0x06],
4922        "Yongnuo YN50mm F1.8N",
4923    ),
4924    (
4925        [0x02, 0x40, 0x44, 0x5C, 0x2C, 0x34, 0x02, 0x00],
4926        "Exakta AF 35-70mm 1:3.5-4.5 MC",
4927    ),
4928    (
4929        [0x07, 0x3E, 0x30, 0x43, 0x2D, 0x35, 0x03, 0x00],
4930        "Soligor AF Zoom 19-35mm 1:3.5-4.5 MC",
4931    ),
4932    (
4933        [0x03, 0x43, 0x5C, 0x81, 0x35, 0x35, 0x02, 0x00],
4934        "Soligor AF C/D Zoom UMCS 70-210mm 1:4.5",
4935    ),
4936    (
4937        [0x12, 0x4A, 0x5C, 0x81, 0x31, 0x3D, 0x09, 0x00],
4938        "Soligor AF C/D Auto Zoom+Macro 70-210mm 1:4-5.6 UMCS",
4939    ),
4940    (
4941        [0x12, 0x36, 0x69, 0x97, 0x35, 0x42, 0x09, 0x00],
4942        "Soligor AF Zoom 100-400mm 1:4.5-6.7 MC",
4943    ),
4944    (
4945        [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01],
4946        "Manual Lens No CPU",
4947    ),
4948    (
4949        [0x00, 0x00, 0x48, 0x48, 0x53, 0x53, 0x00, 0x01],
4950        "Loreo 40mm F11-22 3D Lens in a Cap 9005",
4951    ),
4952    (
4953        [0x00, 0x47, 0x10, 0x10, 0x24, 0x24, 0x00, 0x00],
4954        "Fisheye Nikkor 8mm f/2.8 AiS",
4955    ),
4956    (
4957        [0x00, 0x47, 0x3C, 0x3C, 0x24, 0x24, 0x00, 0x00],
4958        "Nikkor 28mm f/2.8 AiS",
4959    ),
4960    (
4961        [0x00, 0x57, 0x50, 0x50, 0x14, 0x14, 0x00, 0x00],
4962        "Nikkor 50mm f/1.8 AI",
4963    ),
4964    (
4965        [0x00, 0x48, 0x50, 0x50, 0x18, 0x18, 0x00, 0x00],
4966        "Nikkor H 50mm f/2",
4967    ),
4968    (
4969        [0x00, 0x48, 0x68, 0x68, 0x24, 0x24, 0x00, 0x00],
4970        "Series E 100mm f/2.8",
4971    ),
4972    (
4973        [0x00, 0x4C, 0x6A, 0x6A, 0x20, 0x20, 0x00, 0x00],
4974        "Nikkor 105mm f/2.5 AiS",
4975    ),
4976    (
4977        [0x00, 0x48, 0x80, 0x80, 0x30, 0x30, 0x00, 0x00],
4978        "Nikkor 200mm f/4 AiS",
4979    ),
4980    (
4981        [0x00, 0x40, 0x11, 0x11, 0x2C, 0x2C, 0x00, 0x00],
4982        "Samyang 8mm f/3.5 Fish-Eye",
4983    ),
4984    (
4985        [0x00, 0x58, 0x64, 0x64, 0x20, 0x20, 0x00, 0x00],
4986        "Soligor C/D Macro MC 90mm f/2.5",
4987    ),
4988    (
4989        [0x4A, 0x58, 0x30, 0x30, 0x14, 0x0C, 0x4D, 0x02],
4990        "Rokinon 20mm f/1.8 ED AS UMC",
4991    ),
4992    (
4993        [0xA0, 0x56, 0x44, 0x44, 0x14, 0x14, 0xA2, 0x06],
4994        "Sony FE 35mm F1.8",
4995    ),
4996    (
4997        [0xA0, 0x37, 0x5C, 0x8E, 0x34, 0x3C, 0xA2, 0x06],
4998        "Sony FE 70-300mm F4.5-5.6 G OSS",
4999    ),
5000];
5001
5002// ============================================================================
5003// MWG (Metadata Working Group) composite tags
5004// ============================================================================
5005
5006/// MWG tag mapping: (mwg_name, description, sources) where sources are checked
5007/// in priority order. Each source is (group_prefix, tag_name).
5008/// `group_prefix` is matched against family0 or family1; empty means any group.
5009#[allow(clippy::type_complexity)]
5010const MWG_MAPPINGS: &[(&str, &str, &[(&str, &str)])] = &[
5011    (
5012        "Description",
5013        "Description",
5014        &[
5015            ("XMP", "Description"),
5016            ("EXIF", "ImageDescription"),
5017            ("IPTC", "Caption-Abstract"),
5018        ],
5019    ),
5020    (
5021        "DateTimeOriginal",
5022        "Date/Time Original",
5023        &[
5024            ("EXIF", "DateTimeOriginal"),
5025            ("XMP", "DateCreated"),
5026            ("IPTC", "DateCreated"),
5027        ],
5028    ),
5029    (
5030        "CreateDate",
5031        "Create Date",
5032        &[
5033            ("XMP", "CreateDate"),
5034            ("EXIF", "CreateDate"),
5035            ("IPTC", "DigitalCreationDate"),
5036        ],
5037    ),
5038    (
5039        "Copyright",
5040        "Copyright",
5041        &[
5042            ("XMP", "Rights"),
5043            ("EXIF", "Copyright"),
5044            ("IPTC", "CopyrightNotice"),
5045        ],
5046    ),
5047    (
5048        "Creator",
5049        "Creator",
5050        &[("XMP", "Creator"), ("EXIF", "Artist"), ("IPTC", "By-line")],
5051    ),
5052    (
5053        "Keywords",
5054        "Keywords",
5055        &[("XMP", "Subject"), ("IPTC", "Keywords")],
5056    ),
5057    ("City", "City", &[("XMP", "City"), ("IPTC", "City")]),
5058    (
5059        "State",
5060        "State",
5061        &[("XMP", "State"), ("IPTC", "Province-State")],
5062    ),
5063    (
5064        "Country",
5065        "Country",
5066        &[("XMP", "Country"), ("IPTC", "Country-PrimaryLocationName")],
5067    ),
5068    (
5069        "Location",
5070        "Location",
5071        &[("XMP", "Location"), ("IPTC", "Sub-location")],
5072    ),
5073    ("Rating", "Rating", &[("XMP", "Rating")]),
5074];
5075
5076/// Compute MWG composite tags from existing tags.
5077///
5078/// For each MWG mapping, checks sources in priority order and creates a
5079/// composite tag with the first found value.
5080pub fn compute_mwg_composites(tags: &[Tag]) -> Vec<Tag> {
5081    let mut mwg_tags = Vec::new();
5082
5083    for &(mwg_name, description, sources) in MWG_MAPPINGS {
5084        // Find the first matching source tag
5085        let found = sources
5086            .iter()
5087            .find_map(|&(group, tag_name)| find_tag_with_group(tags, tag_name, group));
5088
5089        if let Some(source_tag) = found {
5090            mwg_tags.push(Tag {
5091                id: TagId::Text(format!("MWG:{}", mwg_name)),
5092                name: mwg_name.to_string(),
5093                description: description.to_string(),
5094                group: TagGroup {
5095                    family0: "Composite".to_string(),
5096                    family1: "MWG".to_string(),
5097                    family2: "Other".to_string(),
5098                    family3: "Main".into(),
5099                },
5100                raw_value: source_tag.raw_value.clone(),
5101                print_value: source_tag.print_value.clone(),
5102                priority: 10, // High priority so MWG tags take precedence
5103            });
5104        }
5105    }
5106
5107    mwg_tags
5108}
5109
5110/// Find a tag matching the given name and group prefix.
5111fn find_tag_with_group<'a>(tags: &'a [Tag], name: &str, group: &str) -> Option<&'a Tag> {
5112    let name_lower = name.to_lowercase();
5113    let group_lower = group.to_lowercase();
5114    tags.iter().find(|t| {
5115        t.name.to_lowercase() == name_lower
5116            && (t.group.family0.to_lowercase().contains(&group_lower)
5117                || t.group.family1.to_lowercase().contains(&group_lower))
5118    })
5119}
5120
5121/// MWG write tag mapping: (mwg_tag_name, write_targets).
5122/// Each write target is "Group:TagName" for `set_new_value`.
5123const MWG_WRITE_MAPPINGS: &[(&str, &[&str])] = &[
5124    (
5125        "Description",
5126        &[
5127            "XMP-dc:Description",
5128            "EXIF:ImageDescription",
5129            "IPTC:Caption-Abstract",
5130        ],
5131    ),
5132    (
5133        "DateTimeOriginal",
5134        &[
5135            "EXIF:DateTimeOriginal",
5136            "XMP-photoshop:DateCreated",
5137            "IPTC:DateCreated",
5138        ],
5139    ),
5140    (
5141        "CreateDate",
5142        &[
5143            "XMP-xmp:CreateDate",
5144            "EXIF:CreateDate",
5145            "IPTC:DigitalCreationDate",
5146        ],
5147    ),
5148    (
5149        "Copyright",
5150        &["XMP-dc:Rights", "EXIF:Copyright", "IPTC:CopyrightNotice"],
5151    ),
5152    (
5153        "Creator",
5154        &["XMP-dc:Creator", "EXIF:Artist", "IPTC:By-line"],
5155    ),
5156    ("Keywords", &["XMP-dc:Subject", "IPTC:Keywords"]),
5157    ("City", &["XMP-photoshop:City", "IPTC:City"]),
5158    ("State", &["XMP-photoshop:State", "IPTC:Province-State"]),
5159    (
5160        "Country",
5161        &["XMP-photoshop:Country", "IPTC:Country-PrimaryLocationName"],
5162    ),
5163    ("Location", &["XMP-iptcCore:Location", "IPTC:Sub-location"]),
5164    ("Rating", &["XMP-xmp:Rating"]),
5165];
5166
5167/// Expand an MWG tag name into the list of concrete tags to write.
5168///
5169/// If the tag matches an MWG name (case-insensitive), returns all corresponding
5170/// write targets. Otherwise returns the original tag unchanged.
5171pub fn expand_mwg_write_tag(tag: &str) -> Vec<String> {
5172    // Strip any "MWG:" prefix if present
5173    let bare = if let Some(stripped) = tag.strip_prefix("MWG:") {
5174        stripped
5175    } else {
5176        tag
5177    };
5178
5179    for &(mwg_name, targets) in MWG_WRITE_MAPPINGS {
5180        if bare.eq_ignore_ascii_case(mwg_name) {
5181            return targets.iter().map(|t| t.to_string()).collect();
5182        }
5183    }
5184
5185    // Not an MWG tag, return as-is
5186    vec![tag.to_string()]
5187}
5188
5189#[cfg(test)]
5190mod tests {
5191    use super::*;
5192
5193    /// Helper to create a tag with the given name, group family0/family1, and value.
5194    fn make_tag(name: &str, family0: &str, family1: &str, value: &str) -> Tag {
5195        Tag {
5196            id: TagId::Text(format!("{}:{}", family0, name)),
5197            name: name.to_string(),
5198            description: name.to_string(),
5199            group: TagGroup {
5200                family0: family0.to_string(),
5201                family1: family1.to_string(),
5202                family2: "Other".to_string(),
5203                family3: "Main".into(),
5204            },
5205            raw_value: Value::String(value.to_string()),
5206            print_value: value.to_string(),
5207            priority: 0,
5208        }
5209    }
5210
5211    #[test]
5212    fn test_mwg_description_priority() {
5213        // XMP-dc:Description should take priority over EXIF:ImageDescription
5214        let tags = vec![
5215            make_tag("ImageDescription", "EXIF", "IFD0", "EXIF description"),
5216            make_tag("Description", "XMP", "XMP-dc", "XMP description"),
5217        ];
5218        let mwg = compute_mwg_composites(&tags);
5219        let desc = mwg.iter().find(|t| t.name == "Description");
5220        assert!(desc.is_some(), "MWG Description should be present");
5221        assert_eq!(desc.unwrap().print_value, "XMP description");
5222    }
5223
5224    #[test]
5225    fn test_mwg_no_source_tags() {
5226        // Empty tag list should produce no MWG composites
5227        let mwg = compute_mwg_composites(&[]);
5228        assert!(mwg.is_empty(), "No MWG composites from empty tags");
5229    }
5230
5231    #[test]
5232    fn test_mwg_fallback_to_exif() {
5233        // When only EXIF tag is present, MWG should fall back to it
5234        let tags = vec![make_tag("ImageDescription", "EXIF", "IFD0", "EXIF only")];
5235        let mwg = compute_mwg_composites(&tags);
5236        let desc = mwg.iter().find(|t| t.name == "Description");
5237        assert!(desc.is_some(), "MWG Description should fall back to EXIF");
5238        assert_eq!(desc.unwrap().print_value, "EXIF only");
5239    }
5240
5241    #[test]
5242    fn test_expand_mwg_write_tag() {
5243        // "Description" should expand to XMP, EXIF, and IPTC targets
5244        let targets = expand_mwg_write_tag("Description");
5245        assert_eq!(targets.len(), 3);
5246        assert!(targets.contains(&"XMP-dc:Description".to_string()));
5247        assert!(targets.contains(&"EXIF:ImageDescription".to_string()));
5248        assert!(targets.contains(&"IPTC:Caption-Abstract".to_string()));
5249    }
5250
5251    #[test]
5252    fn test_expand_mwg_write_tag_with_prefix() {
5253        // "MWG:Description" should also expand
5254        let targets = expand_mwg_write_tag("MWG:Description");
5255        assert_eq!(targets.len(), 3);
5256    }
5257
5258    #[test]
5259    fn test_expand_mwg_write_tag_non_mwg() {
5260        // Non-MWG tag should be returned as-is
5261        let targets = expand_mwg_write_tag("Artist");
5262        assert_eq!(targets, vec!["Artist".to_string()]);
5263    }
5264
5265    #[test]
5266    fn test_expand_mwg_write_tag_case_insensitive() {
5267        let targets = expand_mwg_write_tag("description");
5268        assert_eq!(targets.len(), 3);
5269    }
5270
5271    // ── Composite tag computation tests ────────────────────────────
5272
5273    /// Helper to create an EXIF tag with a typed Value.
5274    fn make_exif_tag(name: &str, value: Value, print: &str) -> Tag {
5275        Tag {
5276            id: TagId::Numeric(0),
5277            name: name.to_string(),
5278            description: name.to_string(),
5279            group: TagGroup {
5280                family0: "EXIF".to_string(),
5281                family1: "ExifIFD".to_string(),
5282                family2: "Image".to_string(),
5283                family3: "Main".into(),
5284            },
5285            raw_value: value,
5286            print_value: print.to_string(),
5287            priority: 0,
5288        }
5289    }
5290
5291    fn make_gps_tag(name: &str, value: Value, print: &str) -> Tag {
5292        Tag {
5293            id: TagId::Numeric(0),
5294            name: name.to_string(),
5295            description: name.to_string(),
5296            group: TagGroup {
5297                family0: "EXIF".to_string(),
5298                family1: "GPS".to_string(),
5299                family2: "Location".to_string(),
5300                family3: "Main".into(),
5301            },
5302            raw_value: value,
5303            print_value: print.to_string(),
5304            priority: 0,
5305        }
5306    }
5307
5308    #[test]
5309    fn empty_tags_produce_no_composites() {
5310        let result = compute_composite_tags(&[]);
5311        assert!(result.is_empty());
5312    }
5313
5314    #[test]
5315    fn image_size_from_width_and_height() {
5316        let tags = vec![
5317            make_exif_tag("ImageWidth", Value::U32(1920), "1920"),
5318            make_exif_tag("ImageHeight", Value::U32(1080), "1080"),
5319        ];
5320        let composites = compute_composite_tags(&tags);
5321        let size = composites.iter().find(|t| t.name == "ImageSize");
5322        assert!(size.is_some(), "ImageSize composite not found");
5323        assert_eq!(size.unwrap().print_value, "1920x1080");
5324    }
5325
5326    #[test]
5327    fn megapixels_from_image_size() {
5328        let tags = vec![
5329            make_exif_tag("ImageWidth", Value::U32(4000), "4000"),
5330            make_exif_tag("ImageHeight", Value::U32(3000), "3000"),
5331        ];
5332        let composites = compute_composite_tags(&tags);
5333        let mp = composites.iter().find(|t| t.name == "Megapixels");
5334        assert!(mp.is_some(), "Megapixels composite not found");
5335        assert_eq!(mp.unwrap().print_value, "12.0");
5336    }
5337
5338    #[test]
5339    fn gps_position_from_lat_lon() {
5340        let tags = vec![
5341            make_gps_tag(
5342                "GPSLatitude",
5343                Value::List(vec![
5344                    Value::URational(48, 1),
5345                    Value::URational(51, 1),
5346                    Value::URational(24, 1),
5347                ]),
5348                "48 deg 51' 24\"",
5349            ),
5350            make_gps_tag("GPSLatitudeRef", Value::String("N".into()), "N"),
5351            make_gps_tag(
5352                "GPSLongitude",
5353                Value::List(vec![
5354                    Value::URational(2, 1),
5355                    Value::URational(21, 1),
5356                    Value::URational(7, 1),
5357                ]),
5358                "2 deg 21' 7\"",
5359            ),
5360            make_gps_tag("GPSLongitudeRef", Value::String("E".into()), "E"),
5361        ];
5362        let composites = compute_composite_tags(&tags);
5363        let pos = composites.iter().find(|t| t.name == "GPSPosition");
5364        assert!(pos.is_some(), "GPSPosition composite not found");
5365        let pv = &pos.unwrap().print_value;
5366        assert!(pv.contains("deg"), "expected degrees in: {}", pv);
5367        assert!(pv.contains(", "), "expected comma separator in: {}", pv);
5368    }
5369
5370    #[test]
5371    fn gps_position_south_west_negative() {
5372        let tags = vec![
5373            make_gps_tag(
5374                "GPSLatitude",
5375                Value::List(vec![
5376                    Value::URational(33, 1),
5377                    Value::URational(52, 1),
5378                    Value::URational(0, 1),
5379                ]),
5380                "33 deg 52' 0\" S",
5381            ),
5382            make_gps_tag("GPSLatitudeRef", Value::String("S".into()), "S"),
5383            make_gps_tag(
5384                "GPSLongitude",
5385                Value::List(vec![
5386                    Value::URational(151, 1),
5387                    Value::URational(12, 1),
5388                    Value::URational(0, 1),
5389                ]),
5390                "151 deg 12' 0\" W",
5391            ),
5392            make_gps_tag("GPSLongitudeRef", Value::String("W".into()), "W"),
5393        ];
5394        let composites = compute_composite_tags(&tags);
5395        let pos = composites.iter().find(|t| t.name == "GPSPosition");
5396        assert!(pos.is_some(), "GPSPosition composite not found");
5397        let pv = &pos.unwrap().print_value;
5398        // ExifTool's print form joins the two coordinate print values (which carry
5399        // their N/S/E/W hemisphere suffix) with ", " — it never emits a negative sign.
5400        let parts: Vec<&str> = pv.split(", ").collect();
5401        assert!(
5402            parts.len() == 2 && parts[0].ends_with(" S") && parts[1].ends_with(" W"),
5403            "expected S/W hemisphere suffixes in: {}",
5404            pv
5405        );
5406    }
5407
5408    #[test]
5409    fn shutter_speed_from_exposure_time() {
5410        let tags = vec![make_exif_tag(
5411            "ExposureTime",
5412            Value::URational(1, 125),
5413            "1/125",
5414        )];
5415        let composites = compute_composite_tags(&tags);
5416        let ss = composites.iter().find(|t| t.name == "ShutterSpeed");
5417        assert!(ss.is_some(), "ShutterSpeed composite not found");
5418    }
5419
5420    #[test]
5421    fn aperture_from_fnumber() {
5422        let tags = vec![make_exif_tag("FNumber", Value::URational(28, 10), "2.8")];
5423        let composites = compute_composite_tags(&tags);
5424        let ap = composites.iter().find(|t| t.name == "Aperture");
5425        assert!(ap.is_some(), "Aperture composite not found");
5426        assert_eq!(ap.unwrap().print_value, "2.8");
5427    }
5428
5429    #[test]
5430    fn light_value_from_aperture_shutter_iso() {
5431        let tags = vec![
5432            make_exif_tag("FNumber", Value::URational(4, 1), "4.0"),
5433            make_exif_tag("ExposureTime", Value::URational(1, 125), "1/125"),
5434            make_exif_tag("ISO", Value::U16(100), "100"),
5435        ];
5436        let composites = compute_composite_tags(&tags);
5437        let lv = composites.iter().find(|t| t.name == "LightValue");
5438        assert!(lv.is_some(), "LightValue composite not found");
5439        let val: f64 = lv
5440            .unwrap()
5441            .print_value
5442            .parse()
5443            .expect("LV should be numeric");
5444        assert!((val - 11.0).abs() < 1.0, "LV expected ~11, got: {}", val);
5445    }
5446
5447    #[test]
5448    fn no_gps_without_longitude() {
5449        let tags = vec![make_gps_tag(
5450            "GPSLatitude",
5451            Value::List(vec![
5452                Value::URational(48, 1),
5453                Value::URational(51, 1),
5454                Value::URational(24, 1),
5455            ]),
5456            "48 deg 51' 24\"",
5457        )];
5458        let composites = compute_composite_tags(&tags);
5459        assert!(
5460            composites.iter().all(|t| t.name != "GPSPosition"),
5461            "GPSPosition should not be generated without GPSLongitude"
5462        );
5463    }
5464
5465    #[test]
5466    fn no_image_size_without_height() {
5467        let tags = vec![make_exif_tag("ImageWidth", Value::U32(800), "800")];
5468        let composites = compute_composite_tags(&tags);
5469        assert!(
5470            composites.iter().all(|t| t.name != "ImageSize"),
5471            "ImageSize should not be generated without ImageHeight"
5472        );
5473    }
5474
5475    #[test]
5476    fn composite_tags_in_composite_group() {
5477        let tags = vec![
5478            make_exif_tag("ImageWidth", Value::U32(640), "640"),
5479            make_exif_tag("ImageHeight", Value::U32(480), "480"),
5480        ];
5481        let composites = compute_composite_tags(&tags);
5482        for tag in &composites {
5483            assert_eq!(tag.group.family0, "Composite");
5484            assert_eq!(tag.group.family1, "Composite");
5485        }
5486    }
5487}