View file File name : ppds.py Content :#!/usr/bin/python3 ## system-config-printer ## Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2014, 2015 Red Hat, Inc. ## Copyright (C) 2006 Florian Festi <ffesti@redhat.com> ## Copyright (C) 2006, 2007, 2008, 2009 Tim Waugh <twaugh@redhat.com> ## This program is free software; you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published by ## the Free Software Foundation; either version 2 of the License, or ## (at your option) any later version. ## This program is distributed in the hope that it will be useful, ## but WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ## GNU General Public License for more details. ## You should have received a copy of the GNU General Public License ## along with this program; if not, write to the Free Software ## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. import cups from .cupshelpers import parseDeviceID from . import xmldriverprefs import itertools import string import time import locale import os.path import functools import re from . import _debugprint, set_debugprint_fn from functools import reduce __all__ = ['ppdMakeModelSplit', 'PPDs'] _MFR_BY_RANGE = [ # Fill in missing manufacturer names based on model name ("HP", re.compile("deskjet" "|dj[ 0-9]?" "|laserjet" "|lj" "|color laserjet" "|color lj" "|designjet" "|officejet" "|oj" "|photosmart" "|ps " "|psc" "|edgeline")), ("Epson", re.compile("stylus|aculaser")), ("Apple", re.compile("stylewriter" "|imagewriter" "|deskwriter" "|laserwriter")), ("Canon", re.compile("pixus" "|pixma" "|selphy" "|imagerunner" "|bj" "|lbp")), ("Brother", re.compile("hl|dcp|mfc")), ("Xerox", re.compile("docuprint" "|docupage" "|phaser" "|workcentre" "|homecentre")), ("Lexmark", re.compile("optra|(:color )?jetprinter")), ("KONICA MINOLTA", re.compile("magicolor" "|pageworks" "|pagepro")), ("Kyocera", re.compile("fs-" "|km-" "|taskalfa")), ("Ricoh", re.compile("aficio")), ("Oce", re.compile("varioprint")), ("Oki", re.compile("okipage|microline")) ] _MFR_NAMES_BY_LOWER = {} for mfr, regexp in _MFR_BY_RANGE: _MFR_NAMES_BY_LOWER[mfr.lower ()] = mfr _HP_MODEL_BY_NAME = { "dj": "DeskJet", "lj": "LaserJet", "oj": "OfficeJet", "color lj": "Color LaserJet", "ps ": "PhotoSmart", "hp ": "" } _RE_turboprint = re.compile ("turboprint") _RE_version_numbers = re.compile (r" v(?:er\.)?\d(?:\d*\.\d+)?(?: |$)") _RE_ignore_suffix = re.compile ("," "| hpijs" "| foomatic/" "| - " "| w/" "| \\(" "| postscript" "| ps" "| pdf" "| pxl" "| zjs" # hpcups "| zxs" # hpcups "| pcl3" # hpcups "| printer" # hpcups "|_bt" "| pcl" # Canon CQue "| ufr ii" # Canon UFR II "| br-script" # Brother PPDs ) _RE_ignore_series = re.compile (" series| all-in-one", re.I) def ppdMakeModelSplit (ppd_make_and_model): """ Split a ppd-make-and-model string into a canonical make and model pair. @type ppd_make_and_model: string @param ppd_make_and_model: IPP ppd-make-and-model attribute @return: a string pair representing the make and the model """ # If the string starts with a known model name (like "LaserJet") assume # that the manufacturer name is missing and add the manufacturer name # corresponding to the model name ppd_make_and_model.strip () make = None cleanup_make = False l = ppd_make_and_model.lower () for mfr, regexp in _MFR_BY_RANGE: if regexp.match (l): make = mfr model = ppd_make_and_model break # Handle PPDs provided by Turboprint if make is None and _RE_turboprint.search (l): t = ppd_make_and_model.find (" TurboPrint") if t != -1: t2 = ppd_make_and_model.rfind (" TurboPrint") if t != t2: ppd_make_and_model = ppd_make_and_model[t + 12:t2] else: ppd_make_and_model = ppd_make_and_model[:t] try: make, model = ppd_make_and_model.split("_", 1) except: make = ppd_make_and_model model = '' make = re.sub (r"(?<=[a-z])(?=[0-9])", " ", make) make = re.sub (r"(?<=[a-z])(?=[A-Z])", " ", make) model = re.sub (r"(?<=[a-z])(?=[0-9])", " ", model) model = re.sub (r"(?<=[a-z])(?=[A-Z])", " ", model) model = re.sub (r" Jet", "Jet", model) model = re.sub (r"Photo Smart", "PhotoSmart", model) cleanup_make = True # Special handling for two-word manufacturers elif l.startswith ("konica minolta "): make = "KONICA MINOLTA" model = ppd_make_and_model[15:] elif l.startswith ("lexmark international "): make = "Lexmark" model = ppd_make_and_model[22:] elif l.startswith ("kyocera mita "): make = "Kyocera" model = ppd_make_and_model[13:] elif l.startswith ("kyocera "): make = "Kyocera" model = ppd_make_and_model[8:] elif l.startswith ("fuji xerox "): make = "Fuji Xerox" model = ppd_make_and_model[11:] # Finally, take the first word as the name of the manufacturer. else: cleanup_make = True try: make, model = ppd_make_and_model.split(" ", 1) except: make = ppd_make_and_model model = '' # Standardised names for manufacturers. makel = make.lower () if cleanup_make: if (makel.startswith ("hewlett") and makel.endswith ("packard")): make = "HP" makel = "hp" elif (makel.startswith ("konica") and makel.endswith ("minolta")): make = "KONICA MINOLTA" makel = "konica minolta" else: # Fix case errors. mfr = _MFR_NAMES_BY_LOWER.get (makel) if mfr: make = mfr # HP and Canon PostScript PPDs give NickNames like: # *NickName: "HP LaserJet 4 Plus v2013.111 Postscript (recommended)" # *NickName: "Canon MG4100 series Ver.3.90" # Find the version number and truncate at that point. But beware, # other model names can legitimately look like version numbers, # e.g. Epson PX V500. # Truncate only if the version number has only one digit, or a dot # with digits before and after. modell = model.lower () v = modell.find (" v") if v != -1: # Look for " v" or " ver." followed by a digit, optionally # followed by more digits, a dot, and more digits; and # terminated by a space of the end of the line. vmatch = _RE_version_numbers.search (modell) if vmatch: # Found it -- truncate at that point. vstart = vmatch.start () modell = modell[:vstart] model = model[:vstart] suffix = _RE_ignore_suffix.search (modell) if suffix: suffixstart = suffix.start () modell = modell[:suffixstart] model = model[:suffixstart] # Remove the word "Series" if present. Some models are referred # to as e.g. HP OfficeJet Series 300 (from hpcups, and in the # Device IDs of such models), and other groups of models are # referred to in drivers as e.g. Epson Stylus Color Series (CUPS). (model, n) = _RE_ignore_series.subn ("", model, count=1) if n: modell = model.lower () if makel == "hp": for name, fullname in _HP_MODEL_BY_NAME.items (): if modell.startswith (name): model = fullname + model[len (name):] modell = model.lower () break model = model.strip () return (make, model) def normalize (strin): # This function normalizes manufacturer and model names for comparing. # The string is turned to lower case and leading and trailing white # space is removed. After that each sequence of non-alphanumeric # characters (including white space) is replaced by a single space and # also at each change between letters and numbers a single space is added. # This makes the comparison only done by alphanumeric characters and the # words formed from them. So mostly two strings which sound the same when # you pronounce them are considered equal. Printer manufacturers do not # market two models whose names sound the same but differ only by # upper/lower case, spaces, dashes, ..., but in printer drivers names can # be easily supplied with these details of the name written in the wrong # way, especially if the IEEE-1284 device ID of the printer is not known. # This way we get a very reliable matching of printer model names. # Examples: # - Epson PM-A820 -> epson pm a 820 # - Epson PM A820 -> epson pm a 820 # - HP PhotoSmart C 8100 -> hp photosmart c 8100 # - hp Photosmart C8100 -> hp photosmart c 8100 lstrin = strin.strip ().lower () normalized = "" BLANK=0 ALPHA=1 DIGIT=2 lastchar = BLANK alnumfound = False for i in range (len (lstrin)): if lstrin[i].isalpha (): if lastchar != ALPHA and alnumfound: normalized += " "; lastchar = ALPHA elif lstrin[i].isdigit (): if lastchar != DIGIT and alnumfound: normalized += " "; lastchar = DIGIT else: lastchar = BLANK if lstrin[i].isalnum (): normalized += lstrin[i] alnumfound = True return normalized def _singleton (x): """If we don't know whether getPPDs() or getPPDs2() was used, this function can unwrap an item from a list in either case.""" if isinstance (x, list): return x[0] return x class PPDs: """ This class is for handling the list of PPDs returned by CUPS. It indexes by PPD name and device ID, filters by natural language so that foreign-language PPDs are not included, and sorts by driver type. If an exactly-matching PPD is not available, it can substitute with a PPD for a similar model or for a generic driver. """ # Status of match. STATUS_SUCCESS = 0 STATUS_MODEL_MISMATCH = 1 STATUS_GENERIC_DRIVER = 2 STATUS_NO_DRIVER = 3 FIT_EXACT_CMD = xmldriverprefs.DriverType.FIT_EXACT_CMD FIT_EXACT = xmldriverprefs.DriverType.FIT_EXACT FIT_CLOSE = xmldriverprefs.DriverType.FIT_CLOSE FIT_GENERIC = xmldriverprefs.DriverType.FIT_GENERIC FIT_NONE = xmldriverprefs.DriverType.FIT_NONE _fit_to_status = { FIT_EXACT_CMD: STATUS_SUCCESS, FIT_EXACT: STATUS_SUCCESS, FIT_CLOSE: STATUS_MODEL_MISMATCH, FIT_GENERIC: STATUS_GENERIC_DRIVER, FIT_NONE: STATUS_NO_DRIVER } def __init__ (self, ppds, language=None, xml_dir=None): """ @type ppds: dict @param ppds: dict of PPDs as returned by cups.Connection.getPPDs() or cups.Connection.getPPDs2() @type language: string @param language: language name, as given by the first element of the pair returned by locale.getlocale() """ self.ppds = ppds.copy () self.makes = None self.ids = None self.drivertypes = xmldriverprefs.DriverTypes () self.preforder = xmldriverprefs.PreferenceOrder () if xml_dir is None: xml_dir = os.environ.get ("CUPSHELPERS_XMLDIR") if xml_dir is None: from . import config xml_dir = os.path.join (config.sysconfdir, "cupshelpers") try: xmlfile = os.path.join (xml_dir, "preferreddrivers.xml") (drivertypes, preferenceorder) = \ xmldriverprefs.PreferredDrivers (xmlfile) self.drivertypes.load (drivertypes) self.preforder.load (preferenceorder) except Exception as e: print("Error loading %s: %s" % (xmlfile, e)) self.drivertypes = None self.preforder = None if (language is None or language == "C" or language == "POSIX"): language = "en_US" u = language.find ("_") if u != -1: short_language = language[:u] else: short_language = language to_remove = [] for ppdname, ppddict in self.ppds.items (): try: natural_language = _singleton (ppddict['ppd-natural-language']) except KeyError: continue if natural_language == "en": # Some manufacturer's PPDs are only available in this # language, so always let them though. continue if natural_language == language: continue if natural_language == short_language: continue to_remove.append (ppdname) for ppdname in to_remove: del self.ppds[ppdname] # CUPS sets the 'raw' model's ppd-make-and-model to 'Raw Queue' # which unfortunately then appears as manufacturer Raw and # model Queue. Use 'Generic' for this model. if 'raw' in self.ppds: makemodel = _singleton (self.ppds['raw']['ppd-make-and-model']) if not makemodel.startswith ("Generic "): self.ppds['raw']['ppd-make-and-model'] = "Generic " + makemodel def getMakes (self): """ @returns: a list of strings representing makes, sorted according to the current locale """ self._init_makes () makes_list = list(self.makes.keys ()) makes_list.sort (key=locale.strxfrm) try: # "Generic" should be listed first. makes_list.remove ("Generic") makes_list.insert (0, "Generic") except ValueError: pass return makes_list def getModels (self, make): """ @returns: a list of strings representing models, sorted using cups.modelSort() """ self._init_makes () try: models_list = list(self.makes[make].keys ()) except KeyError: return [] def compare_models (a,b): first = normalize (a) second = normalize (b) return cups.modelSort(first, second) models_list.sort(key=functools.cmp_to_key(compare_models)) return models_list def getInfoFromModel (self, make, model): """ Obtain a list of PPDs that are suitable for use with a particular printer model, given its make and model name. @returns: a dict, indexed by ppd-name, of dicts representing PPDs (as given by cups.Connection.getPPDs) """ self._init_makes () try: return self.makes[make][model] except KeyError: return {} def getInfoFromPPDName (self, ppdname): """ @returns: a dict representing a PPD, as given by cups.Connection.getPPDs """ return self.ppds[ppdname] def getStatusFromFit (self, fit): return self._fit_to_status.get (fit, xmldriverprefs.DriverType.FIT_NONE) def orderPPDNamesByPreference (self, ppdnamelist=None, downloadedfiles=None, make_and_model=None, devid=None, fit=None): """ Sort a list of PPD names by preferred driver type. @param ppdnamelist: PPD names @type ppdnamelist: string list @param downloadedfiles: Filenames from packages downloaded @type downloadedfiles: string list @param make_and_model: device-make-and-model name @type make_and_model: string @param devid: Device ID dict @type devid: dict indexed by Device ID field name, of strings; except for CMD field which must be a string list @param fit: Driver fit string for each PPD name @type fit: dict of PPD name:fit @returns: string list """ if ppdnamelist is None: ppdnamelist = [] if downloadedfiles is None: downloadedfiles = [] if fit is None: fit = {} if self.drivertypes and self.preforder: ppds = {} for ppdname in ppdnamelist: ppds[ppdname] = self.ppds[ppdname] orderedtypes = self.preforder.get_ordered_types (self.drivertypes, make_and_model, devid) _debugprint("Valid driver types for this printer in priority order: %s" % repr(orderedtypes)) orderedppds = self.drivertypes.get_ordered_ppdnames (orderedtypes, ppds, fit) _debugprint("PPDs with assigned driver types in priority order: %s" % repr(orderedppds)) ppdnamelist = [typ_name[1] for typ_name in orderedppds] _debugprint("Resulting PPD list in priority order: %s" % repr(ppdnamelist)) # Special handling for files we've downloaded. First collect # their basenames. downloadedfnames = set() for downloadedfile in downloadedfiles: (path, slash, fname) = downloadedfile.rpartition ("/") downloadedfnames.add (fname) if downloadedfnames: # Next compare the basenames of each ppdname downloadedppdnames = [] for ppdname in ppdnamelist: (path, slash, ppdfname) = ppdname.rpartition ("/") if ppdfname in downloadedfnames: downloadedppdnames.append (ppdname) # Finally, promote the matching ones to the head of the list. if downloadedppdnames: for ppdname in ppdnamelist: if ppdname not in downloadedppdnames: downloadedppdnames.append (ppdname) ppdnamelist = downloadedppdnames return ppdnamelist def getPPDNamesFromDeviceID (self, mfg, mdl, description="", commandsets=None, uri=None, make_and_model=None): """ Obtain a best-effort PPD match for an IEEE 1284 Device ID. @param mfg: MFG or MANUFACTURER field @type mfg: string @param mdl: MDL or MODEL field @type mdl: string @param description: DES or DESCRIPTION field, optional @type description: string @param commandsets: CMD or COMMANDSET field, optional @type commandsets: string @param uri: device URI, optional (only needed for debugging) @type uri: string @param make_and_model: device-make-and-model string @type make_and_model: string @returns: a dict of fit (string) indexed by PPD name """ _debugprint ("\n%s %s" % (mfg, mdl)) orig_mfg = mfg orig_mdl = mdl self._init_ids () if commandsets is None: commandsets = [] # Start with an empty result list and build it up using # several search methods, in increasing order of fuzziness. fit = {} # First, try looking up the device using the manufacturer and # model fields from the Device ID exactly as they appear (but # case-insensitively). mfgl = mfg.lower () mdll = mdl.lower () id_matched = False try: for each in self.ids[mfgl][mdll]: fit[each] = self.FIT_EXACT id_matched = True except KeyError: pass # The HP PPDs say "HP" not "Hewlett-Packard", so try that. if mfgl == "hewlett-packard": try: for each in self.ids["hp"][mdll]: fit[each] = self.FIT_EXACT _debugprint ("**** Incorrect IEEE 1284 Device ID: %s" % self.ids["hp"][mdll]) _debugprint ("**** Actual ID is MFG:%s;MDL:%s;" % (mfg, mdl)) _debugprint ("**** Please report a bug against the HPLIP component") id_matched = True except KeyError: pass # Now try looking up the device by ppd-make-and-model. _debugprint ("Trying make/model names") mdls = None self._init_makes () make = None if mfgl == "": (mfg, mdl) = ppdMakeModelSplit (mdl) mfgl = normalize (mfg) mdll = normalize (mdl) _debugprint ("mfgl: %s" % mfgl) _debugprint ("mdll: %s" % mdll) mfgrepl = {"hewlett-packard": "hp", "lexmark international": "lexmark", "kyocera": "kyocera mita"} if mfgl in self.lmakes: # Found manufacturer. make = self.lmakes[mfgl] elif mfgl in mfgrepl: rmfg = mfgrepl[mfgl] if rmfg in self.lmakes: mfg = rmfg mfgl = mfg # Found manufacturer (after mapping to canonical name) _debugprint ("remapped mfgl: %s" % mfgl) make = self.lmakes[mfgl] _debugprint ("make: %s" % make) if make is not None: mdls = self.makes[make] mdlsl = self.lmodels[normalize(make)] # Remove manufacturer name from model field for prefix in [mfgl, 'hewlett-packard', 'hp']: if mdll.startswith (prefix + ' '): mdl = mdl[len (prefix) + 1:] mdll = normalize (mdl) _debugprint ("unprefixed mdll: %s" % mdll) if mdll in self.lmodels[mfgl]: model = mdlsl[mdll] for each in mdls[model].keys (): fit[each] = self.FIT_EXACT _debugprint ("%s: %s" % (fit[each], each)) else: # Make use of the model name clean-up in the # ppdMakeModelSplit () function (mfg2, mdl2) = ppdMakeModelSplit (mfg + " " + mdl) mdl2l = normalize (mdl2) _debugprint ("re-split mdll: %s" % mdl2l) if mdl2l in self.lmodels[mfgl]: model = mdlsl[mdl2l] for each in list(mdls[model].keys ()): fit[each] = self.FIT_EXACT _debugprint ("%s: %s" % (fit[each], each)) if not fit and mdls: (s, ppds) = self._findBestMatchPPDs (mdls, mdl) if s != self.FIT_NONE: for each in ppds: fit[each] = s _debugprint ("%s: %s" % (fit[each], each)) if commandsets: if type (commandsets) != list: commandsets = commandsets.split (',') _debugprint ("Checking CMD field") generic = self._getPPDNameFromCommandSet (commandsets) if generic: for driver in generic: fit[driver] = self.FIT_GENERIC _debugprint ("%s: %s" % (fit[driver], driver)) # Check by the URI whether our printer is connected via IPP # and if not, remove the PPD entries for driverless printing # (ppdname = "driverless:..." from the list) if (not uri or (not uri.startswith("ipp:") and not uri.startswith("ipps:") and (not uri.startswith("dnssd") or not "._ipp" in uri))): failed = set() for ppdname in fit.keys (): if (ppdname.startswith("driverless:")): failed.add (ppdname) if (len(failed) > 0): _debugprint ("Removed %s due to non-IPP connection" % failed) for each in failed: del fit[each] failed = set() # What about the CMD field of the Device ID? Some devices # have optional units for page description languages, such as # PostScript, and they will report different CMD strings # accordingly. # # By convention, if a PPD contains a Device ID with a CMD # field, that PPD can only be used whenever any of the # comma-separated words in the CMD field appear in the # device's ID. # (See Red Hat bug #630058). # # We'll do that check now, and any PPDs that fail # (e.g. PostScript PPD for non-PostScript printer) can be # eliminated from the list. # # The reason we don't do this check any earlier is that we # don't want to eliminate PPDs only to have the fuzzy matcher # add them back in afterwards. # # While doing this, any drivers that we can positively confirm # as using a command set understood by the printer will be # converted from FIT_EXACT to FIT_EXACT_CMD. if id_matched and len (commandsets) > 0: failed = set() exact_cmd = set() for ppdname in fit.keys (): ppd_cmd_field = None ppd = self.ppds[ppdname] ppd_device_id = _singleton (ppd.get ('ppd-device-id')) if ppd_device_id: ppd_device_id_dict = parseDeviceID (ppd_device_id) ppd_cmd_field = ppd_device_id_dict["CMD"] if (not ppd_cmd_field and # ppd-type is not reliable for driver-generated # PPDs (see CUPS STR #3720). Neither gutenprint # nor foomatic specify ppd-type in their CUPS # drivers. ppdname.find (":") == -1): # If this is a PostScript PPD we know which # command set it will use. ppd_type = _singleton (ppd.get ('ppd-type')) if ppd_type == "postscript": ppd_cmd_field = ["POSTSCRIPT"] if not ppd_cmd_field: # We can't be sure which command set this driver # uses. continue usable = False for pdl in ppd_cmd_field: if pdl in commandsets: usable = True break if usable: exact_cmd.add (ppdname) else: failed.add (ppdname) # Assign the more specific fit "exact-cmd" to those that # positively matched the CMD field. for each in exact_cmd: if fit[each] == self.FIT_EXACT: fit[each] = self.FIT_EXACT_CMD _debugprint (self.FIT_EXACT_CMD + ": %s" % each) if len (failed) < len ([d for (d, m) in fit.items () if m != 'generic']): _debugprint ("Removed %s due to CMD mis-match" % failed) for each in failed: del fit[each] else: _debugprint ("Not removing %s " % failed + "due to CMD mis-match as it would " "leave nothing good") if not fit: fallbacks = ["textonly.ppd", "postscript.ppd"] found = False for fallback in fallbacks: _debugprint ("'%s' fallback" % fallback) fallbackgz = fallback + ".gz" for ppdpath in self.ppds.keys (): if (ppdpath.endswith (fallback) or ppdpath.endswith (fallbackgz)): fit[ppdpath] = self.FIT_NONE found = True break if found: break _debugprint ("Fallback '%s' not available" % fallback) if not found: _debugprint ("No fallback available; choosing any") fit[list(self.ppds.keys ())[0]] = self.FIT_NONE if not id_matched: sanitised_uri = re.sub (pattern="//[^@]*@/?", repl="//", string=str (uri)) try: cmd = reduce (lambda x, y: x + ","+ y, commandsets) except TypeError: cmd = "" id = "MFG:%s;MDL:%s;" % (orig_mfg, orig_mdl) if cmd: id += "CMD:%s;" % cmd if description: id += "DES:%s;" % description _debugprint ("No ID match for device %s:" % sanitised_uri) _debugprint (id) return fit def getPPDNameFromDeviceID (self, mfg, mdl, description="", commandsets=None, uri=None, downloadedfiles=None, make_and_model=None): """ Obtain a best-effort PPD match for an IEEE 1284 Device ID. The status is one of: - L{STATUS_SUCCESS}: the match was successful, and an exact match was found - L{STATUS_MODEL_MISMATCH}: a similar match was found, but the model name does not exactly match - L{STATUS_GENERIC_DRIVER}: no match was found, but a generic driver is available that can drive this device according to its command set list - L{STATUS_NO_DRIVER}: no match was found at all, and the returned PPD name is a last resort @param mfg: MFG or MANUFACTURER field @type mfg: string @param mdl: MDL or MODEL field @type mdl: string @param description: DES or DESCRIPTION field, optional @type description: string @param commandsets: CMD or COMMANDSET field, optional @type commandsets: string @param uri: device URI, optional (only needed for debugging) @type uri: string @param downloadedfiles: filenames from downloaded packages @type downloadedfiles: string list @param make_and_model: device-make-and-model string @type make_and_model: string @returns: an integer,string pair of (status,ppd-name) """ if commandsets is None: commandsets = [] if downloadedfiles is None: downloadedfiles = [] fit = self.getPPDNamesFromDeviceID (mfg, mdl, description, commandsets, uri, make_and_model) # We've got a set of PPDs, any of which will drive the device. # Now we have to choose the "best" one. This is quite tricky # to decide, so let's sort them in order of preference and # take the first. devid = { "MFG": mfg, "MDL": mdl, "DES": description, "CMD": commandsets } ppdnamelist = self.orderPPDNamesByPreference (list(fit.keys ()), downloadedfiles, make_and_model, devid, fit) _debugprint ("Found PPDs: %s" % str (ppdnamelist)) status = self.getStatusFromFit (fit[ppdnamelist[0]]) _debugprint ("Using %s (status: %d)" % (ppdnamelist[0], status)) return (status, ppdnamelist[0]) def _findBestMatchPPDs (self, mdls, mdl): """ Find the best-matching PPDs based on the MDL Device ID. This function could be made a lot smarter. """ _debugprint ("Trying best match") mdll = mdl.lower () if mdll.endswith (" series"): # Strip " series" from the end of the MDL field. mdll = mdll[:-7] mdl = mdl[:-7] best_mdl = None best_matchlen = 0 mdlnames = list(mdls.keys ()) # Perform a case-insensitive model sort on the names. mdlnamesl = [(x, x.lower()) for x in mdlnames] mdlnamesl.append ((mdl, mdll)) mdlnamesl.sort (key=functools.cmp_to_key(lambda x, y: cups.modelSort(x[1], y[1]))) i = mdlnamesl.index ((mdl, mdll)) candidates = [mdlnamesl[i - 1]] if i + 1 < len (mdlnamesl): candidates.append (mdlnamesl[i + 1]) _debugprint (candidates[0][0] + " <= " + mdl + " <= " + candidates[1][0]) else: _debugprint (candidates[0][0] + " <= " + mdl) # Look at the models immediately before and after ours in the # sorted list, and pick the one with the longest initial match. for (candidate, candidatel) in candidates: prefix = os.path.commonprefix ([candidatel, mdll]) if len (prefix) > best_matchlen: best_mdl = list(mdls[candidate].keys ()) best_matchlen = len (prefix) _debugprint ("%s: match length %d" % (candidate, best_matchlen)) # Did we match more than half of the model name? if best_mdl and best_matchlen > (len (mdll) / 2): ppdnamelist = best_mdl if best_matchlen == len (mdll): fit = self.FIT_EXACT else: fit = self.FIT_CLOSE else: fit = self.FIT_NONE ppdnamelist = None # Last resort. Find the "most important" word in the MDL # field and look for a match based solely on that. If # there are digits, try lowering the number of # significant figures. mdlnames.sort (key=functools.cmp_to_key(cups.modelSort)) mdlitems = [(x.lower (), mdls[x]) for x in mdlnames] modelid = None for word in mdll.split (' '): if modelid is None: modelid = word have_digits = False for i in range (len (word)): if word[i].isdigit (): have_digits = True break if have_digits: modelid = word break digits = 0 digits_start = -1 digits_end = -1 for i in range (len (modelid)): if modelid[i].isdigit (): if digits_start == -1: digits_start = i digits_end = i digits += 1 elif digits_start != -1: break digits_end += 1 modelnumber = 0 if digits > 0: modelnumber = int (modelid[digits_start:digits_end]) modelpattern = (modelid[:digits_start] + "%d" + modelid[digits_end:]) _debugprint ("Searching for model ID '%s', '%s' %% %d" % (modelid, modelpattern, modelnumber)) ignore_digits = 0 best_mdl = None found = False while ignore_digits < digits: div = pow (10, ignore_digits) modelid = modelpattern % ((modelnumber / div) * div) _debugprint ("Ignoring %d of %d digits, trying %s" % (ignore_digits, digits, modelid)) for (name, ppds) in mdlitems: for word in name.split (' '): if word.lower () == modelid: found = True break if found: best_mdl = list(ppds.keys ()) break if found: break ignore_digits += 1 if digits < 2: break if found: ppdnamelist = best_mdl fit = self.FIT_CLOSE return (fit, ppdnamelist) def _getPPDNameFromCommandSet (self, commandsets=None): """Return ppd-name list or None, given a list of strings representing the command sets supported.""" if commandsets is None: commandsets = [] try: self._init_makes () models = self.makes["Generic"] except KeyError: return None def get (*candidates): for model in candidates: (s, ppds) = self._findBestMatchPPDs (models, model) if s == self.FIT_EXACT: return ppds return None cmdsets = [x.lower () for x in commandsets] if (("postscript" in cmdsets) or ("postscript2" in cmdsets) or ("postscript level 2 emulation" in cmdsets)): return get ("PostScript") elif (("pclxl" in cmdsets) or ("pcl-xl" in cmdsets) or ("pcl6" in cmdsets) or ("pcl 6 emulation" in cmdsets)): return get ("PCL 6/PCL XL", "PCL Laser") elif "pcl5e" in cmdsets: return get ("PCL 5e", "PCL Laser") elif "pcl5c" in cmdsets: return get ("PCL 5c", "PCL Laser") elif ("pcl5" in cmdsets) or ("pcl 5 emulation" in cmdsets): return get ("PCL 5", "PCL Laser") elif "pcl" in cmdsets: return get ("PCL 3", "PCL Laser") elif (("escpl2" in cmdsets) or ("esc/p2" in cmdsets) or ("escp2e" in cmdsets)): return get ("ESC/P Dot Matrix") return None def _init_makes (self): if self.makes: return tstart = time.time () makes = {} lmakes = {} lmodels = {} aliases = {} # Generic model name: set(specific model names) for ppdname, ppddict in self.ppds.items (): # One entry for ppd-make-and-model ppd_make_and_model = _singleton (ppddict['ppd-make-and-model']) ppd_mm_split = ppdMakeModelSplit (ppd_make_and_model) ppd_makes_and_models = set([ppd_mm_split]) # The ppd-product IPP attribute contains values from each # Product PPD attribute as well as the value from the # ModelName attribute if present. The Product attribute # values are surrounded by parentheses; the ModelName # attribute value is not. # Add another entry for each ppd-product that came from a # Product attribute in the PPD file. ppd_products = ppddict.get ('ppd-product', []) if not isinstance (ppd_products, list): ppd_products = [ppd_products] ppd_products = set ([x for x in ppd_products if x.startswith ("(")]) if ppd_products: # If there is only one ppd-product value it is # unlikely to be useful. if len (ppd_products) == 1: ppd_products = set() make = _singleton (ppddict.get ('ppd-make', '')).rstrip () if make: make += ' ' lmake = normalize (make) for ppd_product in ppd_products: # *Product: attribute is "(text)" if (ppd_product.startswith ("(") and ppd_product.endswith (")")): ppd_product = ppd_product[1:len (ppd_product) - 1] if not ppd_product: continue # If manufacturer name missing, take it from ppd-make lprod = normalize (ppd_product) if not lprod.startswith (lmake): ppd_product = make + ppd_product ppd_makes_and_models.add (ppdMakeModelSplit (ppd_product)) # Add the entries to our dictionary for make, model in ppd_makes_and_models: lmake = normalize (make) lmodel = normalize (model) if lmake not in lmakes: lmakes[lmake] = make lmodels[lmake] = {} makes[make] = {} else: make = lmakes[lmake] if lmodel not in lmodels[lmake]: lmodels[lmake][lmodel] = model makes[make][model] = {} else: model = lmodels[lmake][lmodel] makes[make][model][ppdname] = ppddict # Build list of model aliases if ppd_mm_split in ppd_makes_and_models: ppd_makes_and_models.remove (ppd_mm_split) if ppd_makes_and_models: (make, model) = ppd_mm_split if make in aliases: models = aliases[make].get (model, set()) else: aliases[make] = {} models = set() models = models.union ([x[1] for x in ppd_makes_and_models]) aliases[make][model] = models # Now, for each set of model aliases, add all drivers from the # "main" (generic) model name to each of the specific models. for make, models in aliases.items (): lmake = normalize (make) main_make = lmakes[lmake] for model, modelnames in models.items (): main_model = lmodels[lmake].get (normalize (model)) if not main_model: continue main_ppds = makes[main_make][main_model] for eachmodel in modelnames: this_model = lmodels[lmake].get (normalize (eachmodel)) ppds = makes[main_make][this_model] ppds.update (main_ppds) self.makes = makes self.lmakes = lmakes self.lmodels = lmodels _debugprint ("init_makes: %.3fs" % (time.time () - tstart)) def _init_ids (self): if self.ids: return ids = {} for ppdname, ppddict in self.ppds.items (): id = _singleton (ppddict.get ('ppd-device-id')) if not id: continue id_dict = parseDeviceID (id) lmfg = id_dict['MFG'].lower () lmdl = id_dict['MDL'].lower () bad = False if len (lmfg) == 0: bad = True if len (lmdl) == 0: bad = True if bad: continue if lmfg not in ids: ids[lmfg] = {} if lmdl not in ids[lmfg]: ids[lmfg][lmdl] = [] ids[lmfg][lmdl].append (ppdname) self.ids = ids def _show_help(): print ("usage: ppds.py [--deviceid] [--list-models] [--list-ids] [--debug]")