/usr/bin/lensfun-add-adapter is in liblensfun-bin 0.3.2-4.
This file is owned by root:root, with mode 0o755.
The actual contents of the file can be viewed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 | #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""This program allows the user to add further mount compatibilities to his
Lensfun configuration. This way, the number of default mount compatibilities
can be kept low, while users who own adapters can add those to their local
installation. Especially mirrorless systems can use all SLR lenses, so the
lens lists to choose from would become very long.
The program can be used non-interactively (passing arguments on the command
line) and interactively, where the program asks you questions and takes your
input.
For example, you own a Sony NEX-7 and have bought an adapter to use A-mount
lenses with it. Thus, you start this program. The program asks for a camera
model name. You enter "NEX-7". Then, you get a list of possible mounts,
together with numbers. In this list, it says::
32) Sony Alpha
So, you enter "32 <RETURN>". That's it. Lensfun now offers also A-mount
lenses for E-mount cameras (all, by the way, not only for the NEX-7).
Diagnostics:
Status code 0 -- successful
1 -- invalid command line arguments
2 -- invalid input in interactive mode
The program writes its output to ``~/.local/share/lensfun/_mounts.xml``. Any
former mount configuration in that file is preserved. Since it can be assumed
that this file will not change its format often (in particular, not with every
new Lensfun DB version), this file is deliberately not put in a versioned
sub-directory. As a positive side-effect of this, the user is not forced to
move this file once a new Lensfun DB version comes out.
"""
import os, sys, argparse, glob
from xml.etree import ElementTree
import lensfun
local_xml_filepath = os.path.expanduser("~/.local/share/lensfun/_mounts.xml")
os.makedirs(os.path.dirname(local_xml_filepath), exist_ok=True)
parser = argparse.ArgumentParser(description="Add mount compatibilities (adapters) to Lensfun's database.")
parser.add_argument("--maker", help="Exact camera manufacturer name.")
parser.add_argument("--model", help="Exact camera model name.")
parser.add_argument("--mount", help="Exact mount name to add as an adapter for camera mount.")
parser.add_argument("--remove-local-mount-config", action="store_true",
help="Remove all local changes to the mount compatibility configuration made through this program.")
args = parser.parse_args()
if args.maker and not args.model or args.model and not args.maker:
print("ERROR: The options --maker and --camera must be passed together.")
sys.exit(1)
if args.remove_local_mount_config:
if args.maker or args.model or args.mount:
print("ERROR: If the option --remove-local-mount-config is given, it must be the only one.")
sys.exit(1)
def default_name(element, tag_name):
"""Returns the untranslated string in the tag `tag_name`, which is a
sub-element of `element`. This is useful if you need the e.g. definitive
model name of a camera in order to use it as a key.
"""
for child in element.findall(tag_name):
if "lang" not in child.attrib:
return child.text
def fancy_name(element, tag_name):
"""Returns the English version of the string in the tag `tag_name`, which is a
sub-element of `element`. If not available, take the untranslated version.
"""
for child in element.findall(tag_name):
if child.attrib.get("lang") == "en":
return child.text
return default_name(element, tag_name)
def normalize_camera_name(name):
return name.lower().translate(str.maketrans("", "", " ,.:-+*/()[]"))
def indent(root, level=0):
"""Pretty-print an XML tree by indentation."""
i = "\n" + level * " "
if len(root):
if not root.text or not root.text.strip():
root.text = i + " "
if not root.tail or not root.tail.strip():
root.tail = i
for root in root:
indent(root, level + 1)
if not root.tail or not root.tail.strip():
root.tail = i
else:
if level and (not root.tail or not root.tail.strip()):
root.tail = i
class Mount:
"""Normally, all data structures in this program are just strings or mappings
or lists of strings. Even compatibility mounts are only represented by
their name. However, for the main list of mounts, we have to store more of
them. Therefore, this is a class.
"""
def __init__(self, name, fixed_lens=False):
self.name, self.fixed_lens = name, fixed_lens
self.compatible_mounts = set()
def __str__(self):
return self.name
def read_database():
"""Read the database of Lensfun. Note that also the local configuration –
including the ``_mounts.xml`` file – is read.
It returns the cameras as a mapping from the (maker, model) tuple to the
mount object, the `makers_and_models` mapping which maps so-called “loose”
camera names to (maker, model) tuples, and the mounts as a mapping from
mount names to `Mount` objects.
“Loose” camera names are brutally normalised to that partial string
matching can be easily done against user input. In particular, everything
is lowercase and all punctuation and spacing is removed.
"""
cameras = {}
makers_and_models = {}
mounts = {}
paths = lensfun.get_database_directories()
for path in paths:
for filepath in glob.glob(os.path.join(path, "*.xml")):
filename = os.path.basename(filepath)
tree = ElementTree.parse(filepath)
for mount in tree.findall("mount"):
name = default_name(mount, "name")
try:
fixed_lens = mounts[name].fixed_lens
except KeyError:
fixed_lens = filename.startswith("compact") or name in ["olympusE10"]
mounts[name] = Mount(name, fixed_lens)
for compatible_mount in mount.findall("compat"):
mounts[name].compatible_mounts.add(compatible_mount.text)
for camera in tree.findall("camera"):
loose_name = normalize_camera_name(fancy_name(camera, "maker") + fancy_name(camera, "model"))
maker_and_model = default_name(camera, "maker"), default_name(camera, "model")
makers_and_models[loose_name] = maker_and_model
mount = default_name(camera, "mount")
cameras[maker_and_model] = mount
mounts.setdefault(mount, Mount(mount, filename.startswith("compact") or mount in ["olympusE10"]))
for maker_and_model, mount_name in cameras.items():
cameras[maker_and_model] = mounts[mount_name]
return mounts, makers_and_models, cameras
def find_mount_groups(mounts):
"""A “mount group” is a set of mount names that are fully compatible to
every other one in the group. This way, we can offer the user to add all
Nikon lenses in one go to their camera mount, no matter whether the Nikon
lens is Nikon F, Nikon F AI-S, etc. Remember that mount compatibilities
are not transitive in Lensfun.
"""
groups = set()
for mount in mounts.values():
if mount.name != "Generic":
mutually_compatibles = {mount.name}
for compatible_mount in mount.compatible_mounts:
try:
reverse_compatibles = mounts[compatible_mount].compatible_mounts
except KeyError:
continue
if compatible_mount != "Generic" and mount.name in reverse_compatibles:
mutually_compatibles.add(compatible_mount)
if len(mutually_compatibles) > 1:
groups.add(frozenset(mutually_compatibles))
return groups
def find_to_mount(cameras, makers_and_models):
"""The `to_mount` is the camera mount, i.e. the mount *to* which we want to
adapt.
"""
if args.model:
try:
return cameras[args.maker, args.model]
except KeyError:
print("ERROR: Camera model not found in database.")
sys.exit(1)
else:
camera_name = normalize_camera_name(input("Enter camera model name (or part of it): "))
hits = []
for loose_name, maker_and_model in makers_and_models.items():
if camera_name in loose_name:
hits.append(maker_and_model)
if not hits:
print("ERROR: No cameras with this name found.")
sys.exit(2)
elif len(hits) > 1:
hits.sort()
for i, maker_and_model in enumerate(hits, 1):
print("{:3d}) {} {}".format(i, *maker_and_model))
try:
number = int(input("Enter number: "))
maker_and_model = hits[number - 1]
except (ValueError, IndexError, EOFError):
print("ERROR: Invalid input.")
sys.exit(2)
else:
maker_and_model = hits[0]
return cameras[maker_and_model]
def find_from_mounts(mounts, groups, to_mount):
"""The `from_mount` is the lenses' mount, i.e. the mount *from* which we want
to adapt. It may be a set of mounts in case of a mount group.
"""
if args.mount:
if args.mount not in mounts:
print("ERROR: Mount name not found in database.")
sys.exit(1)
return {args.mount}
else:
from_candidates = []
for from_candidate in [(mount.name, {mount.name}) for mount in mounts.values() if not mount.fixed_lens] + \
[(", ".join(sorted(group)) + " (all together)", group) for group in groups]:
for mount in from_candidate[1]:
if mount not in to_mount.compatible_mounts | {to_mount.name}:
break
else:
continue
from_candidates.append(from_candidate)
from_candidates.sort()
for i, mount_data in enumerate(from_candidates, 1):
print("{:3d}) {}".format(i, mount_data[0]))
try:
number = int(input("Enter number: "))
from_mounts = from_candidates[number - 1][1]
except (ValueError, IndexError, EOFError):
print("ERROR: Invalid input.")
sys.exit(2)
return from_mounts
def write_xml_file(to_mount, from_mounts, mounts):
"""We write the ``_mounts.xml`` file rather non-invasive, i.e. we touch only
the mount we want to change. Either we change it, or we add it.
"""
try:
tree = ElementTree.parse(local_xml_filepath)
except (OSError, ElementTree.ParseError):
tree = ElementTree.ElementTree(ElementTree.Element("lensdatabase"))
for mount in tree.findall("mount"):
if default_name(mount, "name") == to_mount.name:
for compatible_mount in mount.findall("compat"):
mount.remove(compatible_mount)
break
else:
mount = ElementTree.SubElement(tree.getroot(), "mount")
ElementTree.SubElement(mount, "name").text = to_mount.name
for compatible_mount in sorted(to_mount.compatible_mounts):
ElementTree.SubElement(mount, "compat").text = compatible_mount
indent(tree.getroot())
tree.write(local_xml_filepath)
if args.remove_local_mount_config:
try:
os.remove(local_xml_filepath)
except OSError:
pass
else:
mounts, makers_and_models, cameras = read_database()
mount_groups = find_mount_groups(mounts)
to_mount = find_to_mount(cameras, makers_and_models)
if to_mount.fixed_lens:
from_mounts = {"Generic"}
else:
from_mounts = find_from_mounts(mounts, mount_groups, to_mount)
to_mount.compatible_mounts.update(from_mounts)
write_xml_file(to_mount, from_mounts, mounts)
print("Made {} lenses mountable to {}.".format(" / ".join(from_mounts), to_mount))
|