Skip to content

Instantly share code, notes, and snippets.

@arthursoares
Created January 31, 2025 21:37
Show Gist options
  • Save arthursoares/68130f5de2de8f252431675fee0d9e44 to your computer and use it in GitHub Desktop.
Save arthursoares/68130f5de2de8f252431675fee0d9e44 to your computer and use it in GitHub Desktop.
Leica M: 6bit encoding EXIF Fixer for 3rd-party lenses

Leica 6-Bit EXIF Fixer

This script was developed by ChatGPT o1 with human suppervision of Arthur Soares Website / Threads :).

Overview

On a Leica M digital camera, lenses are normally identified via an internal “6-bit encoding.” Leica’s native lenses have unique codes, and the camera writes lens info to the file’s EXIF metadata. However, third-party lenses (Voigtländer, Zeiss, etc.) often lack an official Leica 6-bit code, so photographers commonly “reuse” a Leica lens code with similar focal length and aperture. This results in Leica Summicron lens info showing up in the metadata even when you’re shooting with, say, a Voigtländer 35 mm f/2 Ultron.

This script uses ExifTool to:

  1. Find .DNG files whose EXIF/XMP fields mention “Summicron” (or older Voigtländer labels).
  2. Overwrite lens-related fields so they correctly identify your third-party lens (e.g., “Voigtlander VM 35mm f/2 Ultron Aspherical”), including focal length, aperture, and brand name.
  3. Update Adobe-specific lens-profile fields (XMP-crs) to reflect a custom lens correction profile rather than the default Leica Summicron reference.

Requirements

Installation / Compilation

  1. Save the code below in a file named, for example, fix_6bit_exif.go.
  2. In a terminal, run:
    go build fix_6bit_exif.go

How It Works

  1. Search: The script runs ExifTool with -if conditions that look for Summicron references in EXIF or XMP metadata (e.g., "Summicron-M 1:2/35 ASPH.") or older Voigtländer strings.
  2. Rewrite: If there’s a match, the script sets the following fields to your desired Voigtländer lens name and specs:
    • EXIF: LensMake, LensModel, FocalLength, LensInfo
    • XMP-AUX: aux:Lens
    • XMP-CRS: LensProfileName, LensProfileFilename, LensProfileDigest, etc.
  3. Output:
    • After a preview (if you use -dry-run), the script overwrites .DNG metadata in place, so any future software (Lightroom, Photoshop, etc.) sees the correct lens info.

Fields Updated

EXIF Fields

  • LensMake: e.g., "Voigtlander".
  • LensModel: e.g., "Voigtlander VM 35mm f/2 Ultron Aspherical".
  • FocalLength: numeric focal length, e.g., "35.0mm".
  • LensInfo: condensed specs, e.g., "35mm f/2".

XMP-AUX Field

  • XMP-aux:Lens: Adobe-specific lens name. Must be updated or Lightroom might still show Summicron.

XMP-CRS (Camera Raw Settings) Fields

  • LensProfileSetup: typically set to Custom.
  • LensProfileName: descriptive string, e.g., "Adobe (Voigtlander VM 35mm f/2 Ultron Aspherical)".
  • LensProfileFilename: the .lcp file name, e.g., "Leica Camera AG (Voigtlander VM 35mm f2 Ultron Aspherical) - RAW.lcp".
  • LensProfileDigest: a unique hash for the lens profile (e.g., "03CBD374CCB89A292AD832BB830E440F").
  • LensProfileIsEmbedded: usually False unless it’s truly embedded.

Why It Matters

  • EXIF readers, Lightroom, etc., will incorrectly list Summicron in the file info.
  • Lens corrections in Adobe Camera Raw may apply the wrong correction profile if you leave Summicron metadata untouched.

By correcting the metadata, you ensure all software knows the true lens model, focal length, and aperture. You can also load or reference a custom lens profile for your third-party lens rather than Leica’s Summicron corrections.

FAQ

What if exit code 2 is a real parameter error?

By default, we treat both 1 and 2 as “no matches,” which means we skip them. If you want to fail on a genuine parameter error, remove the code || code == 2 in runExifToolIgnoreNoMatches.

How do I add a new lens?

Just append a new LensFix to the lensFixes array. For example:

{
  Condition: LensCondition{
    AuxLensValues: []string{
      "Summicron-M 1:2/50 ASPH.",
      // etc.
    },
    CrsProfileValues: []string{
      "Adobe (Leica SUMMICRON-M 50 mm f/2 ASPH.)",
    },
  },
  LensMake:        "Zeiss",
  LensName:        "Zeiss ZM 50mm f/2 Planar",
  FocalLength:     "50.0mm",
  Aperture:        "f/2",
  // etc.
},

How do I only match .DNG files, not .jpg or others?

We already do -ext dng in the script. That ensures ExifTool only processes DNG files.

Will this overwrite in place?

Yes. -overwrite_original modifies the original files and prevents .DNG_original backups. If you want backups, remove that flag or pass your own ExifTool arguments.

How do I know how Lightroom / Leica is tagging the files?

You can run the command exiftool -a -u -G1 -s FILENAME.DNG to print out the EXIF values and find the fields you want to update.

How do I know which corected profile Adobe Lightroom uses for the lens I want to add?

Manually change the Lens Correction in Lightroom, make sure you "write the metadate to file", and then run exiftool -a -u -G1 -s FILENAME.DNG to see the LensProfileName, LensProfileFilename, and LensProfileDigest fields.


Enjoy accurate lens metadata for your Voigtländer (or other third-party) lenses!

package main
import (
"bufio"
"flag"
"fmt"
"os"
"os/exec"
"strings"
)
// LensCondition lists old-lens strings to match in XMP-aux:Lens or XMP-crs:LensProfileName.
type LensCondition struct {
AuxLensValues []string
CrsProfileValues []string
}
// LensFix describes how to correct a certain Summicron -> (Voigtlander/Zeiss/etc.) lens.
type LensFix struct {
Condition LensCondition
// The new lens metadata you want to apply:
LensMake string // e.g. "Voigtlander", "Zeiss", "TTArtisan", etc.
LensName string // e.g. "Voigtlander VM 35mm f/2 Ultron Aspherical"
FocalLength string // e.g. "35.0mm"
Aperture string // e.g. "f/2"
ProfileName string // e.g. "Adobe (Voigtlander VM 35mm f/2 Ultron Aspherical)"
ProfileFilename string // e.g. "Leica Camera AG (Voigtlander VM 35mm f2 Ultron) - RAW.lcp"
ProfileDigest string // e.g. "03CBD3..."
}
// LensCondition -> single-line expression: ($XMP-aux:Lens eq 'X' or $XMP-crs:LensProfileName eq 'Y' ...)
func buildIfExpression(cond LensCondition) string {
var parts []string
// For XMP-aux:Lens
for _, val := range cond.AuxLensValues {
parts = append(parts, fmt.Sprintf("$XMP-aux:Lens eq '%s'", val))
}
// For XMP-crs:LensProfileName
for _, val := range cond.CrsProfileValues {
parts = append(parts, fmt.Sprintf("$XMP-crs:LensProfileName eq '%s'", val))
}
joined := strings.Join(parts, " or ")
return "(" + joined + ")"
}
// runExifToolIgnoreNoMatches runs exiftool, ignoring codes 1 or 2 => "no matches" or environment quirks.
func runExifToolIgnoreNoMatches(args []string) error {
cmd := exec.Command("exiftool", args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
code := exitErr.ExitCode()
// If exiftool returns 1 or 2 => treat it as no-matches => skip
if code == 1 || code == 2 {
return nil
}
}
return err
}
return nil
}
func promptYN(prompt string) bool {
reader := bufio.NewReader(os.Stdin)
fmt.Print(prompt)
line, _ := reader.ReadString('\n')
return strings.EqualFold(strings.TrimSpace(line), "y")
}
// We no longer store LensMake in a common map—some might not be Voigtlander.
var commonFields = map[string]string{
// XMP-crs defaults remain the same
"XMP-crs:LensProfileSetup": "Custom",
"XMP-crs:LensProfileIsEmbedded": "False",
}
// Summicron->LensFix mappings
var lensFixes = []LensFix{
{
Condition: LensCondition{
AuxLensValues: []string{
"Summicron-M 1:2/28 ASPH.",
"Voigtlander VM 28mm f/2 Ultron",
},
CrsProfileValues: []string{
"Adobe (Leica SUMMICRON-M 28 mm f/2 ASPH.)",
},
},
LensMake: "Voigtlander",
LensName: "Voigtlander VM 28mm f/2 Ultron",
FocalLength: "28.0mm",
Aperture: "f/2",
ProfileName: "Adobe (Voigtlander VM 28mm f/2 Ultron)",
ProfileFilename: "Leica Camera AG (Voigtlander VM 28mm f2 Ultron) - RAW.lcp",
ProfileDigest: "5887B71917B64C63B836F9B3A430D0F6",
},
{
Condition: LensCondition{
AuxLensValues: []string{
"Summicron-M 1:2/35 ASPH.",
"Voigtlander VM Ultron 35mm f/2 Aspherical ii",
"Voigtlander VM 35mm f/2 Ultron Aspherical",
},
CrsProfileValues: []string{
"Adobe (Leica SUMMICRON-M 35 mm f/2 ASPH.)",
},
},
LensMake: "Voigtlander",
LensName: "Voigtlander VM 35mm f/2 Ultron Aspherical",
FocalLength: "35.0mm",
Aperture: "f/2",
ProfileName: "Adobe (Voigtlander VM 35mm f/2 Ultron Aspherical)",
ProfileFilename: "Leica Camera AG (Voigtlander VM 35mm f2 Ultron Aspherical) - RAW.lcp",
ProfileDigest: "03CBD374CCB89A292AD832BB830E440F",
},
// If you have a Zeiss lens or TTArtisan, etc., just add another entry here
}
// merges common + lens-specific fields
func buildTagMap(fix LensFix) map[string]string {
tags := make(map[string]string)
// copy common
for k, v := range commonFields {
tags[k] = v
}
// lens-specific
tags["LensMake"] = fix.LensMake
tags["LensModel"] = fix.LensName
tags["XMP-aux:Lens"] = fix.LensName
// e.g. "35.0mm" => "35mm f/2"
focal := strings.TrimSuffix(fix.FocalLength, "mm") // => "35.0"
focal = strings.TrimRight(strings.TrimRight(focal, "0"), ".") // => "35"
tags["FocalLength"] = fix.FocalLength
tags["LensInfo"] = focal + "mm " + fix.Aperture
tags["XMP-crs:LensProfileName"] = fix.ProfileName
tags["XMP-crs:LensProfileFilename"] = fix.ProfileFilename
tags["XMP-crs:LensProfileDigest"] = fix.ProfileDigest
return tags
}
func main() {
dryRun := flag.Bool("dry-run", false, "Only preview which files match; do not modify.")
helpFlag := flag.Bool("help", false, "Show help message and exit.")
hFlag := flag.Bool("h", false, "Show help message and exit.")
flag.Usage = func() {
fmt.Fprintf(os.Stderr, `Usage: %[1]s [options] [directories...]
Options:
-dry-run Only list matching files; do not modify.
-help, -h Show this help message and exit.
Description:
Correct Leica Summicron-labeled DNGs to updated lens metadata.
We handle possibly missing or incorrectly assigned lens info,
and ignore exiftool exit codes 1 and 2 if no matches are found.
Examples:
# Preview in current dir
%[1]s -dry-run .
# Actually fix files in /path/to
%[1]s /path/to
`, os.Args[0])
}
flag.Parse()
if *helpFlag || *hFlag {
flag.Usage()
os.Exit(0)
}
targets := flag.Args()
if len(targets) == 0 {
targets = []string{"."}
}
// 1) Dry-run
for _, fix := range lensFixes {
fmt.Println("===> Checking for files whose XMP-aux:Lens is ANY OF:")
for _, val := range fix.Condition.AuxLensValues {
fmt.Printf(" - %s\n", val)
}
fmt.Println(" OR whose XMP-crs:LensProfileName is ANY OF:")
for _, val := range fix.Condition.CrsProfileValues {
fmt.Printf(" - %s\n", val)
}
expr := buildIfExpression(fix.Condition)
args := []string{
"-T",
"-ext", "dng",
"-if", expr,
"-Filename",
"-XMP-aux:Lens",
"-XMP-crs:LensProfileName",
}
args = append(args, targets...)
if err := runExifToolIgnoreNoMatches(args); err != nil {
fmt.Fprintf(os.Stderr, "\nError in dry run for lens: %q => %v\n", fix.LensName, err)
os.Exit(1)
}
fmt.Println("\n------------------------------------------------------\n")
}
if *dryRun {
fmt.Println("===> DRY RUN COMPLETE. No changes were made.")
return
}
// 2) Confirm
fmt.Println("Above are the matches for the known Summicron->Lens fixes.")
if !promptYN("===> Proceed to OVERWRITE metadata? (y/N) ") {
fmt.Println("Aborted.")
return
}
// 3) Real updates
fmt.Println("\n===> Applying metadata updates...")
baseArgs := []string{"-overwrite_original", "-ext", "dng"}
finalArgs := append([]string{}, baseArgs...)
for i, fix := range lensFixes {
expr := buildIfExpression(fix.Condition)
finalArgs = append(finalArgs, "-if", expr)
tmap := buildTagMap(fix)
for k, v := range tmap {
finalArgs = append(finalArgs, fmt.Sprintf("-%s=%s", k, v))
}
if i < len(lensFixes)-1 {
finalArgs = append(finalArgs, "-execute")
finalArgs = append(finalArgs, baseArgs...)
}
}
finalArgs = append(finalArgs, targets...)
if err := runExifToolIgnoreNoMatches(finalArgs); err != nil {
fmt.Fprintf(os.Stderr, "Error applying metadata changes: %v\n", err)
os.Exit(1)
}
fmt.Println("\nAll done!")
}
@arthursoares
Copy link
Author

ExifFixer

This is the result of running the script.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment