pyppd-1.0.2/0000775000175000017500000000000012214435150013175 5ustar vitorvitor00000000000000pyppd-1.0.2/CHANGES.txt0000664000175000017500000004516312214435150015017 0ustar vitorvitor00000000000000v1.0.2 (2013-09-12) * setup.py, pyppd/runner.py, pyppd/pyppd-ppdfile.in: Changing version to 1.0.2 * MANIFEST.in: Without it, the ISSUES aren't packed in the distribution file, and setup.py breaks as it needs it to generate the long_description. * contrib/git2changes.py: Fix git2changes when commits only modified one file v1.0.1 (2013-09-05) * setup.py, pyppd/runner.py, pyppd/pyppd-ppdfile.in: Changing version to 1.0.1 * pyppd/ppd.py: - if a device ID has no MDL/MODEL field, it is not taken into account as an extra model - If in a device ID a DRV: field is found (Foomatic info) this is added to the device IDs of all model entries. - Model names get normalized before comparing: all lowercase, leading and trailing white space stripped, manufacturer name in the beginning removed, then only really different models get an extra entry. - If a PPD contains one or more "*1284DeviceID:" lines and only one "*Product:" This removes tons of bogus lines, especially in the PPD from Foomatic )both generated from XML and ready-made PPDs from printer manufacturers) and HPLIP.1 v1.0.0 (2012-06-15) * setup.py, pyppd/runner.py: pyppd is being used for a couple years now, so it is stable. And, with Martin Pitt's patches (thanks!), it's working for Python 3. So I guess it deserves this big version bump :) * README: They have to have 4 spaces to be treated as code. * setup.py, ISSUES.txt, ISSUES: Renaming ISSUES.txt -> ISSUES. * setup.py, README.txt, README.md, README: GitHub only parses as Markdown if it's README.md, but PyPI complains if it's neither README nor README.txt. To make everyone happy, I symlinked. * setup.py: Changing repository URL to github. * README.txt: Updating README.txt about the Python 3.x support. * setup.py: Changing pypi status to Production/Stable. * setup.py, LICENSE.txt: This might make pyppd more useful for the *BSD guys. * setup.py: setup.py build crashed with UnicodeDecodeError, as open() defaults to the system locale, and README.txt contains non-ASCII characters. * pyppd/runner.py, pyppd/pyppd-ppdfile.in, pyppd/ppd.py, pyppd/compressor.py, pyppd/archiver.py: Use bytes for reading and writing the archive and PPD files. Do not try to decode/encode whole PPDs. Many of them are not in UTF-8, and not even in the encoding they claim to be in, so it's best to not assume anything about their encoding at all and just keep them as bytes. Only decode the minimal pieces This makes pyppd work with Python 3 and is also more robust against encoding Avoid calling str() on a PPD object, as that will cause UTF-8 encoding again on an unicode object. Just call __str__() directly. This is a bit ugly, but works with both Python 2 and 3. * setup.py, pyppd/pyppd-ppdfile.in, pyppd/ppd.py, pyppd/archiver.py: Fix syntax to be Python 3 compatible. * pyppd/runner.py, pyppd/pyppd-ppdfile.in, pyppd/archiver.py: Fix import statements to be Python 3 compatible. * pyppd/pyppd-ppdfile.in: Only exit if the IOError caught was a Broken Pipe. * pyppd/pyppd-ppdfile.in, bin/pyppd: 1. pyppd-ppdfile now catches IO errors at the actual print commands and not globally for the whole program. So real bugs will not be masked by the 2. pyppd catches only KeyboardInterrupt as it is writing to an actual disk file and so here the crash on broken pipe problem does not exist (a better error handling would still be nice though). v0.4.9 (2010-09-21) * setup.py, pyppd/runner.py, pyppd/pyppd-ppdfile.in: Changing version to 0.4.9 * pyppd/pyppd-ppdfile.in, bin/pyppd: KeyboardInterrupts are raised when the user uses, for example, CTRL+C. But when you do "./pyppd-ppdfile list | less" and, before the end of file, types "q", it raises an IOError. We don't want neither throwing a traceback into stdout. Thanks for the tip, Till. * bin/pyppd: Oops! Error in the indentation. v0.4.8 (2010-09-20) * setup.py, pyppd/runner.py, pyppd/pyppd-ppdfile.in: Changing version to 0.4.8 * pyppd/pyppd-ppdfile.in, bin/pyppd: As noted by Till Kamppeter, generating a traceback triggers an automatic bug report feature in some distributions (like Ubuntu). We avoid generating false bug reports by suppressing those KeyboardInterrupt's tracebacks. v0.4.7 (2010-09-20) * setup.py, pyppd/runner.py, pyppd/pyppd-ppdfile.in: Changing version to 0.4.7 * setup.py: Removing useless whitespaces. * contrib/git2changes.py: Removes ending whitespace in CHANGES.txt's commit messages. * pyppd/pyppd-ppdfile.in, pyppd/ppd.py: Make sure that the "pyppd-ppdfile list" output does not contain two lines with the same PPD URI * ISSUES.txt: Adds missing period. v0.4.6 (2010-08-17) * setup.py, pyppd/runner.py, pyppd/pyppd-ppdfile.in: Changing version to 0.4.6 * setup.py: If we don't do this, the resulting string isn't valid rST. v0.4.5 (2010-08-17) * setup.py, pyppd/runner.py, pyppd/pyppd-ppdfile.in: Changing version to 0.4.5 * ISSUES.txt: Adds "Generate manpage" issue. * setup.py: Letting our issues be known to everyone might encourage contributions. * ISSUES.txt: Minor esthetic changes. * README.txt: Adds Till, Hin-Tak, Flávio, Diógenes and OSPO as Contributors. * README.txt: Adds # and $ to indicate commands done in the shell. * README.txt: Adds multiple archives usage's instructions. v0.4.4 (2010-08-11) * setup.py, pyppd/runner.py, pyppd/pyppd-ppdfile.in: Changing version to 0.4.4 * pyppd/pyppd-ppdfile.in, pyppd/ppd.py: 1. The PPD parser confused "NickName" and "ShortNickName". This leads to the "ShortNickName" being used in "pyppd-ppdfile list" and so important information is missing in the listings and the listings are 2. "pyppd-ppdfile list" lists PPD URIs with the full path of pyppd-ppdfile, usually /usr/lib/cups/driver/pyppd-ppdfile. This makes the PPD generator not working correctly with CUPS. v0.4.3 (2010-08-10) * setup.py, pyppd/runner.py, pyppd/pyppd-ppdfile.in: Changing version to 0.4.3 * pyppd/ppd.py, pyppd/archiver.py: Renaming filename -> uri. * pyppd/archiver.py: So we avoid the problem of having PPDs with the same filename. It's working as: if I call pyppd with the folder /usr/share/ppd, it removes this part of each PPD path and uses the rest as the URI. For example, /usr/share/ppd/cups-included/textonly.ppd.gz will have the URI "cups-included/textonly.ppd". v0.4.2 (2010-08-09) * setup.py, pyppd/runner.py, pyppd/pyppd-ppdfile.in: Changing version to 0.4.2 * pyppd/archiver.py: Oops! Forgot to change the variable name. * setup.py: Adds classifiers to setup.py. * ISSUES.txt: Removing resolved issue. * pyppd/runner.py: I don't think logging to a file is worthwile to pyppd, as it isn't a daemon. If you encounter some problem, you'll be running it yourself, Also, I prefer using -v and -vv instead of -v and -d. I think it makes things more standard. * pyppd/runner.py, pyppd/ppd.py, pyppd/archiver.py, ISSUES.txt: Now, pyppd prints to stdout the WARNING, ERROR and FATAL messages, and logs those and every other to a log file (defaults to pyppd.log), which has a size limit of 2 MB. You can use --verbose and --debug to make pyppd more verbose. v0.4.1 (2010-08-09) * setup.py, pyppd/runner.py, pyppd/pyppd-ppdfile.in: Changing version to 0.4.1 * pyppd/archiver.py: When we have two PPDs with the same filename, we create add a random number to one of them, so we can add both to our archive. But, if this PPD which'll have the new name have more than one entry in the list, each entry will have a different random name. This commit fixes this. * ISSUES.txt: Adds new issue. * ISSUES.txt: "setup.py install" shouldn't link pyppd to /usr/lib/cups/driver. What should be there isn't pyppd, but the archive generated by it. v0.4.0 (2010-08-09) * setup.py, pyppd/runner.py, pyppd/pyppd-ppdfile.in: Changing version to 0.4.0 * pyppd/ppd.py: Now, when parsing a PPD, it adds as many *1284DeviceID lines as it can find. Afterwards, it looks for *Product lines. If found, check if it's unique (if there wasn't an *1284DeviceID line with the same product model), and adds it if so. * pyppd/archiver.py, ISSUES.txt: If we found a PPD with the same filename, we simply add a random number (between 0 and 99) to the end of its filename. * pyppd/pyppd-ppdfile.in: Renaming index -> value. v0.3.0 (2010-08-07) * setup.py, pyppd/runner.py, pyppd/pyppd-ppdfile.in: Changing version to 0.3.0 * pyppd/ppd.py, pyppd/archiver.py, ISSUES.txt: There are some of PPDs which are in different files but contains the same "*PCFileName:". So, without this change, pyppd overwritten them in the index, making them impossible to recover. Now, it'll only happen when there're two PPDs with the same basename. I'm also writing at ISSUES.txt that we need to figure out how to solve this possible problem. * pyppd/ppd.py: Fixing typo deviceid -> deviceid_re. * pyppd/ppd.py, pyppd/archiver.py: Renaming name -> filename. * pyppd/pyppd-ppdfile.in, pyppd/ppd.py, pyppd/archiver.py: As Till Kamppeter pointed out, a single PPD might have multiple descriptions, following the rules: a) If the PPD has an "*1284DeviceID:" line, create a single list entry with this device ID; b) If the PPD has one or more "*Product:" lines, create a pseudo device ID using the content of the "*Manufacturer:" line as manufacturer and "*Product:" line as model, whichout the parentheses. * ISSUES.txt: Removing "support for .ppd.gz" from our ISSUES list. v0.2.2 (2010-08-07) * setup.py, pyppd/runner.py, pyppd/pyppd-ppdfile.in: Changing version to 0.2.2 * pyppd/archiver.py, README.txt: Now, instead of looking for only *.ppd in the directory passed as parameter, it looks also for *.ppd.gz. When found, it tests: does it ends with ".gz" (case-sensitive (I have to change this))? If so, opens with gzip. If not, opens as usual. When we archive gzipped PPDs, we save them uncompressed. * pyppd/archiver.py: Refactoring some variable names. v0.2.1 (2010-08-07) * setup.py, pyppd/runner.py, pyppd/pyppd-ppdfile.in: Changing version to 0.2.1 * pyppd/archiver.py: Updating archiver.compress() documentation. v0.2.0 (2010-08-07) * setup.py, pyppd/runner.py, pyppd/pyppd-ppdfile.in: Changing version to 0.2.0 * pyppd/archiver.py: Removing useless try/except block. * pyppd/pyppd-ppdfile.in, pyppd/archiver.py: Instead of creating a .tar archive, we simply concatenate all PPDs found in a big text file, and save each's start and length inside of it. Then, when we have to "cat" it, we decompress the text file and print only In my tests, it's around 8% faster than using .tar. Also, it makes it easier, in the future, to support not only files .ppd, but also .ppd.gz. * pyppd/runner.py, pyppd/archiver.py: Rename method create_archive -> archive * pyppd/archiver.py: Reorganizing methods, the most important at the top * pyppd/compressor.py: Updating decompress() documentation * pyppd/pyppd-ppdfile.in, pyppd/lzma_proxy.py, pyppd/compressor.py, pyppd/archiver.py: Renaming lzma_proxy.py to compressor.py * pyppd/runner.py, pyppd/compressor.py, pyppd/archiver.py: The runner.py shouldn't have to use compressor.py. It should simply ask archiver.py to create the archive. Compression is an implementation detail of archiver. * pyppd/runner.py, pyppd/pyppd-ppdfile.in: Fixing version number 0.1.0 -> 0.1.6 * pyppd/ppd.py: Removing space between DeviceID's field name and content. * setup.py, contrib/git2changes.py: I made git2changes which simply gets the commit messages and organizes them into something similar to the GNU ChangeLog style. But, instead of separating the commits by day, I organize them by version. I think this way, even as it's not standard, generates a file that is more useful to pyppd's users. * CHANGES.txt, .gitignore: As CHANGES.txt is created from the git log, we have no use to maintain it under version control. v0.1.6 (2010-08-03) * setup.py: Changing version to 0.1.6 * MANIFEST.in: So, pyppd/pyppd-ppdfile.in gets into the distribution archive. * pyppd/pyppd-ppdfile.in: As now we don't uncompress the .tar.xz archive at ls(), when we tested if ppd.__class__ == tarfile.TarFile, it always failed. Now, at the iteration, we get the key AND the value (ppd). If the key is ARCHIVE, we continue. * ISSUES.txt: Adding Till's suggestions and some ideas to ISSUES.txt * setup.py: Making setup.py executable v0.1.5 (2010-08-01) * setup.py: Adding a description and bumping version to 0.1.4 v0.1.4 (2010-08-01) * setup.py: Changing version to 0.1.4 * README.txt: Adding instructions to install with pip v0.1.3 (2010-08-01) * setup.py: Changing version to 0.1.3 * pyppd/compressor.py: Instead of silently ignoring exceptions, raises them * README.txt: Minor reStructuredText fixes v0.1.1 (2010-08-01) * setup.py: Renaming the license in setup.py to GPLv3 * setup.py: Forgot to add read() to the long_description * setup.py, README.txt: Adding a README.txt and setting it as long_description * pyppd/pyppd-ppdfile.in: When you tried ./pyppd-ppdfile cat my-ppd.ppd, it raised a IndexError, as it tried to "my-ppd.ppd".split(":")[1], which don't exists. Now, instead of trying to read the second element ([1]), we read the last element, which works in both cases. * setup.py, pyppd/test/__init__.py: Remove pyppd/test, as I am not using tests (yet) * ISSUES.txt: As Gitorious don't offers us one. Maybe we should consider switching to github or something... * pyppd/pyppd-ppdfile.in: As the file lzma_proxy.py is inserted into pyppd-compressor, we don't use "import lzma_proxy as lzma". So, the methods are in the local namespace, thus we use compress/decompress, and not lzma.compress/decompress. * pyppd/lzma_proxy.py: Now, I need to change it's name. Lzma_proxy seems unnatural to me, and we already use compressor for another file. Maybe xz.py? * pyppd/pyppd-ppdfile.in: We used to decompress the PPDs in load(). So, even if the user simply wanted to list the PPDs (which we use only the index), he had to decompress everything. Now, only when the user asks to cat a file, we decompress the PPDs. * pyppd/pyppd-ppdfile.in, pyppd/compressor.py: This way, we compress 212mb to 3.2mb, instead of 8,2mb. This makes the program a bit slower (0.2s in my tests), but I think it's worthy. * pyppd/runner.py, pyppd/pyppd-ppdfile.in: Adding the \ to concatenate the multiline strings * pyppd/pyppd-ppdfile.in: Checks if pyppd-ppdfile is run directly and, if so, run main() * pyppd/pyppd-ppdfile.in: The name 'list()' is used for instantiate list objects. * pyppd/ppd.py: Ppd: Add object inheritance * pyppd/runner.py, pyppd/pyppd-ppdfile.in: Fitting to 80 columns * pyppd/archiver.py: Archiver: Removes unused import * pyppd/compressor.py: When we add the PPDs to the .tar file, /usr/share/ppd becomes usr/share/ppd. This way, we were unable to find it afterwards. Now, we convert every path passed as argument to absolute, and simply ignore the first slash when creating the index. * pyppd/ppd.py: Removes the "(" and ")" from the Product line * pyppd/ppd.py: Use Product instead of NickName in the 1284DeviceID fallback * pyppd/pyppd-ppdfile.in, pyppd/compressor.py: It's better than the sqlite3 solution because we can decompress and load the tar file from memory, without the need to write to decompress, write As tar doesn't have an index, it needs to do a sequential search to find the files. It might be better to use 7z instead. * pyppd/lzma_proxy.py: Adds compress_file(path), which compresses path with xz binary * pyppd/compressor.py: This way, sqlite3 indexes its values, making the selects much faster. * pyppd/pyppd-ppdfile.in, pyppd/compressor.py: It's much faster to load the sqlite3 DB and begin issuing SQL statements than to load the .sql dump into a new sqlite3 DB. * pyppd/pyppd-ppdfile.in, pyppd/compressor.py: cPickle uses too much memory when pickling/unpickling. And, as we work with large files, it becomes a problem. SQLite3 is more efficient in this regard. * pyppd/runner.py: Fixing bug of undefined variable when running pyppd with unexistent directory as argument * pyppd/ppd.py: This is probably wrong [1] but, as I found many PPDs which 1284DeviceID [1] http://www.undocprint.org/formats/communication_protocols/ieee_1284#ieee_1284_device_ * pyppd/compressor.py: Sets ppds = None when we don't need it anymore, trying to keep memory usage low. * pyppd/ppd.py: Adding languages not found in ISO 639, but found in some PPDs * pyppd/ppd.py: Converts PPD's language name to it's ISO 639 code * pyppd/ppd.py, pyppd/compressor.py: When compressing, ignore the PPDs which couldn't be parsed. * pyppd/pyppd-ppdfile.in, pyppd/ppd.py: Instead of having the PPDs names hardcoded with the executable's, prefix them at execution time. Also, remove the "./" of executable's name, when called like ./pyppd-ppdfile. * setup.py, pyppd/runner.py, pyppd/pyppd-ppdfile.in, pyppd/archiver.py: Now, everything will be in only one file by default. When running pyppd, it'll parse the folder passed as argument and create, by default, pyppd-ppdfile. You can run it with list or cat URI to get the compressed PPDs. * pyppd/runner.py: Refactoring (extract method parse_args()) * pyppd/ppd.py: We don't need to save the ppd_file anymore * pyppd/compressor.py: So, when loading the resulting pickle file, we don't need the PPD class definition. We just load a tuple with two strings. * pyppd/runner.py: Adds command-line argument parsing * pyppd/ppd.py, pyppd/compressor.py: PPD class now saves its PPD file in an attribute * pyppd/lzma_proxy.py: communicate() returns a tuple (stdout, stderr). Returns stdout. * pyppd/compressor.py: Refactoring import lzma_proxy * pyppd/ppd.py, pyppd/compressor.py: The PPD class parses a string with the ppd, looking for its attributes. Now, instead of saving a dictionary with ppd_path => {size: ppd_size, start: ppd_start}, it creates a PPD object for each ppd, with it's attributes, so we can in the future list them as foomatic does. * pyppd/lzma_proxy.py, pyppd/compressor.py: This way, we can fallback to using the xz binary, if there isn't python-lzma available. It also has the advantage of using the fastest, if both are available. I need to do some tests to check which is better. * pyppd/compressor.py: Adding compressor.compress() docstring. * pyppd/compressor.py: Forgot the final period. * pyppd/compressor.py: Removing garbage * pyppd/compressor.py: Minor typo fix v0.1.0 (2010-06-12) * setup.py, pyppd/test/__init__.py, pyppd/runner.py, pyppd/compressor.py, pyppd/__init__.py, bin/pyppd, MANIFEST.in, LICENSE.txt, CHANGES.txt, .gitignore: Initial release. pyppd-1.0.2/PKG-INFO0000664000175000017500000000624112214435150014275 0ustar vitorvitor00000000000000Metadata-Version: 1.1 Name: pyppd Version: 1.0.2 Summary: A CUPS PostScript Printer Driver's compressor and generator Home-page: http://github.com/vitorbaptista/pyppd/ Author: Vitor Baptista Author-email: vitor@vitorbaptista.com License: MIT Description: pyppd ===== ``pyppd`` is a CUPS PPD generator. It holds an compressed archive of PPDs, which can be listed and retrieved only when needed by CUPS, saving disk space. Instalation ----------- To install ``pyppd``, you can use: # pip install pyppd Or download the source package, uncompress, and run as root: # python setup.py install It depends on Python 2.x or 3.x (http://www.python.org) and XZ Utils (http://tukaani.org/xz/). Usage ----- At first, you have to create a PPD archive. For such, put all PPDs (they might be gzipped) you want to add in the archive inside a single folder (which can have subfolders), then run: $ pyppd /path/to/your/ppd/folder It'll create ``pyppd-ppdfile`` in your current folder. This executable only works with the same Python version that you used to generate it. You can test it by running: $ ./pyppd-ppdfile list And, for reading a PPD from the archive, simply do: $ ./pyppd-ppdfile cat pyppd-ppdfile:MY-PPD-FILE.PPD For CUPS to be able to use your newly-created archive, copy ``pyppd-ppdfile`` to ``/usr/lib/cups/driver/`` and you're done. The generated ``pyppd-ppdfile`` can be arbitrarily renamed, so that more than one packed repository can be installed on one system. This can be useful if you need a better performance, be it in time or memory usage. Note that also the PPD URIs will follow the new name: $ ./pyppd-ppdfile list pyppd-ppdfile:LasterStar/LaserStar-XX100.ppd $ mv pyppd-ppdfile laserstar $ ./laserstar list laserstar:LaserStar/LaserStar-XX100.ppd Contributors ------------ * **Till Kamppeter** - Original idea, mentoring and feedback. User #0. * **Hin-Tak Leung** - Lots of technical suggestions. * **Martin Pitt** - Python 3 port. * **Flávio Ribeiro** and **Diógenes Fernandes** - Refactorings and general Python's best practices tips. * **Google's OSPO** - Initial funding at GSoC 2010. Issues ------ * Add unit tests. * When testing if the PPD generated from the \*Product line already exists in a \*1284DeviceID line, we do a case-sensitive test. I think it should be case- insensitive. * Generate manpage. Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: System Administrators Classifier: Operating System :: POSIX Classifier: License :: OSI Approved :: MIT License Classifier: Topic :: Printing pyppd-1.0.2/bin/0000775000175000017500000000000012214435150013745 5ustar vitorvitor00000000000000pyppd-1.0.2/bin/pyppd0000775000175000017500000000027112211124032015016 0ustar vitorvitor00000000000000#!/usr/bin/env python from pyppd import runner try: runner.run() except KeyboardInterrupt: # We don't want a KeyboardInterrupt throwing a traceback # into stdout. pass pyppd-1.0.2/LICENSE.txt0000664000175000017500000000204212211124032015005 0ustar vitorvitor00000000000000Copyright (c) 2012 Vitor Baptista Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. pyppd-1.0.2/README0000664000175000017500000000356512211124032014055 0ustar vitorvitor00000000000000pyppd ===== ``pyppd`` is a CUPS PPD generator. It holds an compressed archive of PPDs, which can be listed and retrieved only when needed by CUPS, saving disk space. Instalation ----------- To install ``pyppd``, you can use: # pip install pyppd Or download the source package, uncompress, and run as root: # python setup.py install It depends on Python 2.x or 3.x (http://www.python.org) and XZ Utils (http://tukaani.org/xz/). Usage ----- At first, you have to create a PPD archive. For such, put all PPDs (they might be gzipped) you want to add in the archive inside a single folder (which can have subfolders), then run: $ pyppd /path/to/your/ppd/folder It'll create ``pyppd-ppdfile`` in your current folder. This executable only works with the same Python version that you used to generate it. You can test it by running: $ ./pyppd-ppdfile list And, for reading a PPD from the archive, simply do: $ ./pyppd-ppdfile cat pyppd-ppdfile:MY-PPD-FILE.PPD For CUPS to be able to use your newly-created archive, copy ``pyppd-ppdfile`` to ``/usr/lib/cups/driver/`` and you're done. The generated ``pyppd-ppdfile`` can be arbitrarily renamed, so that more than one packed repository can be installed on one system. This can be useful if you need a better performance, be it in time or memory usage. Note that also the PPD URIs will follow the new name: $ ./pyppd-ppdfile list pyppd-ppdfile:LasterStar/LaserStar-XX100.ppd $ mv pyppd-ppdfile laserstar $ ./laserstar list laserstar:LaserStar/LaserStar-XX100.ppd Contributors ------------ * **Till Kamppeter** - Original idea, mentoring and feedback. User #0. * **Hin-Tak Leung** - Lots of technical suggestions. * **Martin Pitt** - Python 3 port. * **Flávio Ribeiro** and **Diógenes Fernandes** - Refactorings and general Python's best practices tips. * **Google's OSPO** - Initial funding at GSoC 2010. pyppd-1.0.2/pyppd/0000775000175000017500000000000012214435150014331 5ustar vitorvitor00000000000000pyppd-1.0.2/pyppd/archiver.py0000664000175000017500000000743512211124032016506 0ustar vitorvitor00000000000000import base64 import sys import os import fnmatch import gzip import logging from random import randint import pyppd.compressor import pyppd.ppd try: import cPickle as pickle except ImportError: import pickle def archive(ppds_directory): """Returns a string with the decompressor, its dependencies and the archive. It reads the template at pyppd/pyppd-ppdfile.in, inserts the dependencies and the archive encoded in base64, and returns as a string. """ logging.info('Compressing folder "%s".' % ppds_directory) ppds_compressed = compress(ppds_directory) if not ppds_compressed: return None ppds_compressed_b64 = base64.b64encode(ppds_compressed) logging.info('Populating template.') template = read_file_in_syspath("pyppd/pyppd-ppdfile.in") compressor_py = read_file_in_syspath("pyppd/compressor.py") template = template.replace(b"@compressor@", compressor_py) template = template.replace(b"@ppds_compressed_b64@", ppds_compressed_b64) return template def compress(directory): """Compresses and indexes *.ppd and *.ppd.gz in directory returning a string. The directory is walked recursively, concatenating all ppds found in a string. For each, it tests if its filename ends in *.gz. If so, opens with gzip. If not, opens directly. Then, it parses and saves its name, description (in the format CUPS needs (which can be more than one)) and it's position in the ppds string (start position and length) into a dictionary, used as an index. Then, it compresses the string, adds into the dictionary as key ARCHIVE and returns a compressed pickle dump of it. """ ppds = b"" ppds_index = {} abs_directory = os.path.abspath(directory) for ppd_path in find_files(directory, ("*.ppd", "*.ppd.gz")): # Remove 'directory/' from the filename ppd_filename = ppd_path[len(abs_directory)+1:] if ppd_path.lower().endswith(".gz"): ppd_file = gzip.open(ppd_path).read() # We don't want the .gz extension in our filename ppd_filename = ppd_filename[:-3] else: ppd_file = open(ppd_path, 'rb').read() start = len(ppds) length = len(ppd_file) logging.debug('Found %s (%d bytes).' % (ppd_path, length)) ppd_parsed = pyppd.ppd.parse(ppd_file, ppd_filename) ppd_descriptions = [p.__str__() for p in ppd_parsed] ppds_index[ppd_parsed[0].uri] = (start, length, ppd_descriptions) logging.debug('Adding %d entry(ies): %s.' % (len(ppd_descriptions), ppd_descriptions)) ppds += ppd_file if not ppds: logging.error('No PPDs found in folder "%s".' % directory) return None logging.info('Compressing archive.') ppds_index['ARCHIVE'] = pyppd.compressor.compress(ppds) logging.info('Generating and compressing pickle dump.') ppds_pickle = pyppd.compressor.compress(pickle.dumps(ppds_index)) return ppds_pickle def read_file_in_syspath(filename): """Reads the file in filename in each sys.path. If we couldn't find, throws the last IOError caught. """ last_exception = None for path in sys.path: try: return open(path + "/" + filename, 'rb').read() except IOError as ex: last_exception = ex continue raise last_exception def find_files(directory, patterns): """Yields each file that matches any of patterns in directory.""" logging.debug('Searching for "%s" files in folder "%s".' % (", ".join(patterns), directory)) abs_directory = os.path.abspath(directory) for root, dirnames, filenames in os.walk(abs_directory): for pattern in patterns: for filename in fnmatch.filter(filenames, pattern): yield os.path.join(root, filename) pyppd-1.0.2/pyppd/ppd.py0000664000175000017500000001723712212071456015503 0ustar vitorvitor00000000000000import re import logging LANGUAGES = {'afar': 'aa', 'abkhazian': 'ab', 'afrikaans': 'af', 'amharic': 'am', 'arabic': 'ar', 'assamese': 'as', 'aymara': 'ay', 'azerbaijani': 'az', 'bashkir': 'ba', 'byelorussian': 'be', 'bulgarian': 'bg', 'bihari': 'bh', 'bislama': 'bi', 'bengali': 'bn', 'bangla': 'bn', 'tibetan': 'bo', 'breton': 'br', 'catalan': 'ca', 'corsican': 'co', 'czech': 'cs', 'welsh': 'cy', 'danish': 'da', 'german': 'de', 'bhutani': 'dz', 'greek': 'el', 'english': 'en', 'esperanto': 'eo', 'spanish': 'es', 'estonian': 'et', 'basque': 'eu', 'persian': 'fa', 'finnish': 'fi', 'fiji': 'fj', 'faeroese': 'fo', 'french': 'fr', 'frisian': 'fy', 'irish': 'ga', 'scots gaelic': 'gd', 'galician': 'gl', 'guarani': 'gn', 'gujarati': 'gu', 'hausa': 'ha', 'hindi': 'hi', 'croatian': 'hr', 'hungarian': 'hu', 'armenian': 'hy', 'interlingua': 'ia', 'interlingue': 'ie', 'inupiak': 'ik', 'indonesian': 'in', 'icelandic': 'is', 'italian': 'it', 'hebrew': 'iw', 'japanese': 'ja', 'yiddish': 'ji', 'javanese': 'jw', 'georgian': 'ka', 'kazakh': 'kk', 'greenlandic': 'kl', 'cambodian': 'km', 'kannada': 'kn', 'korean': 'ko', 'kashmiri': 'ks', 'kurdish': 'ku', 'kirghiz': 'ky', 'latin': 'la', 'lingala': 'ln', 'laothian': 'lo', 'lithuanian': 'lt', 'latvian': 'lv','lettish': 'lv', 'malagasy': 'mg', 'maori': 'mi', 'macedonian': 'mk', 'malayalam': 'ml', 'mongolian': 'mn', 'moldavian': 'mo', 'marathi': 'mr', 'malay': 'ms', 'maltese': 'mt', 'burmese': 'my', 'nauru': 'na', 'nepali': 'ne', 'dutch': 'nl', 'norwegian': 'no', 'occitan': 'oc', '(afan) oromo': 'om', 'oriya': 'or', 'punjabi': 'pa', 'polish': 'pl', 'pashto': 'ps', 'pushto': 'ps', 'portuguese': 'pt', 'quechua': 'qu', 'rhaeto-romance': 'rm', 'kirundi': 'rn', 'romanian': 'ro', 'russian': 'ru', 'kinyarwanda': 'rw', 'sanskrit': 'sa', 'sindhi': 'sd', 'sangro': 'sg', 'serbo-croatian': 'sh', 'singhalese': 'si', 'slovak': 'sk', 'slovenian': 'sl', 'samoan': 'sm', 'shona': 'sn', 'somali': 'so', 'albanian': 'sq', 'serbian': 'sr', 'siswati': 'ss', 'sesotho': 'st', 'sundanese': 'su', 'swedish': 'sv', 'swahili': 'sw', 'tamil': 'ta', 'tegulu': 'te', 'tajik': 'tg', 'thai': 'th', 'tigrinya': 'ti', 'turkmen': 'tk', 'tagalog': 'tl', 'setswana': 'tn', 'tonga': 'to', 'turkish': 'tr', 'tsonga': 'ts', 'tatar': 'tt', 'twi': 'tw', 'ukrainian': 'uk', 'urdu': 'ur', 'uzbek': 'uz', 'vietnamese': 'vi', 'volapuk': 'vo', 'wolof': 'wo', 'xhosa': 'xh', 'yoruba': 'yo', 'chinese': 'zh', 'simplified chinese': 'zh_TW', 'traditional chinese': 'zh_CN', 'zulu': 'zu', 'portuguese_brazil': 'pt_BR'} class PPD(object): """Represents a PostScript Description file.""" def __init__(self, uri, language, manufacturer, nickname, deviceid): """Initializes a PPD object with the information passed.""" self.uri = uri self.language = language self.manufacturer = manufacturer self.nickname = nickname self.deviceid = deviceid def __str__(self): return '"%s" %s "%s" "%s" "%s"' % (self.uri, self.language, self.manufacturer, self.nickname, self.deviceid) def parse(ppd_file, filename): """Parses ppd_file and returns an array with the PPDs it found. One ppd_file might result in more than one PPD. The rules are: return an PPD for each "1284DeviceID" entry, and one for each "Product" line, if it creates an unique (Manufacturer, Product) DeviceID. """ def standardize(model_name): # Consider it the same model if the product name differs only by # upper/lower case and by the presence/absence of the manufacturer # name return model_name.lower().replace("Hewlett-Packard ".lower(), "").replace("%s " % manufacturer.lower(), "").strip() logging.debug('Parsing %s.' % filename) language_re = re.search(b'\*LanguageVersion:\s*(.+)', ppd_file) manufacturer_re = re.search(b'\*Manufacturer:\s*"(.+)"', ppd_file) nickname_re = re.search(b'\*NickName:\s*"(.+)"', ppd_file) deviceids = re.findall(b'\*1284DeviceID:\s*"(.+)"', ppd_file) try: language = LANGUAGES[language_re.group(1).decode('UTF-8', errors='replace').strip().lower()] manufacturer = manufacturer_re.group(1).strip().decode('UTF-8', errors='replace') nickname = nickname_re.group(1).strip().decode('UTF-8', errors='replace') logging.debug('Language: "%s", Manufacturer: "%s", Nickname: "%s".' % (language, manufacturer, nickname)) ppds = [] models = [] drventry = None line = 0 num_device_ids = 0 num_products = 0 if deviceids: for deviceid in deviceids: deviceid = deviceid.decode('UTF-8', errors='replace') logging.debug('1284DeviceID: "%s".' % deviceid) if (not deviceid.endswith(";")): deviceid += ";" uri = "%d/%s" % (line, filename) # Save a DRV field (from Foomatic) and use it for all entries # of this PPD newdrventry = re.findall(".*DRV:\s*(.*?)\s*;.*", deviceid, re.I) if (len(newdrventry) > 0): drventry = newdrventry[0] elif (drventry != None): deviceid += "DRV:%s;" % drventry newmodels = re.findall(".*(?:MODEL|MDL):\s*(.*?)\s*;.*", deviceid, re.I) if (newmodels): newmodels = list(map(standardize, newmodels)) if (len(newmodels) > 0): # Consider only IDs with a MODEL/MDL field ppds += [PPD(uri, language, manufacturer, nickname, deviceid.strip())] models += newmodels num_device_ids += 1 line += 1 for product in re.findall(b'\*Product:\s*"\(\s*(.+?)\s*\)"', ppd_file): product = product.strip().decode('UTF-8', errors='replace') # Don't add a new entry if there's already one for the same # product/model product_standardized = standardize(product) if product_standardized in models: logging.debug('Ignoring already found *Product: "%s".' % product) continue logging.debug('Product: "%s"' % product) deviceid = "MFG:%s;MDL:%s;" % (manufacturer, product) if (drventry != None): deviceid += "DRV:%s;" % drventry uri = "%d/%s" % (line, filename) ppds += [PPD(uri, language, manufacturer, nickname, deviceid)] num_products += 1 line += 1 models += [product_standardized] if (num_products == 1 and num_device_ids > 0): # If there is at least one device ID and only one Product line, do # not consider the Product line as another model than the one # represented by the device ID and use only the data of the device # ID. ppds.pop() return ppds except: raise Exception("Error parsing PPD file '%s'" % filename) pyppd-1.0.2/pyppd/pyppd-ppdfile.in0000664000175000017500000000617312214434534017452 0ustar vitorvitor00000000000000#!/usr/bin/env python # compressor.py @compressor@ # compressor.py import os import sys from optparse import OptionParser from sys import argv import base64 try: import cPickle as pickle except ImportError: import pickle from io import BytesIO from os.path import basename from errno import EPIPE def load(): ppds_compressed = base64.b64decode(ppds_compressed_b64) ppds_decompressed = decompress(ppds_compressed) ppds = pickle.loads(ppds_decompressed) return ppds def ls(): binary_name = basename(argv[0]) ppds = load() for key, value in ppds.items(): if key == 'ARCHIVE': continue for ppd in value[2]: try: print(ppd.replace('"', '"' + binary_name + ':', 1)) except IOError as e: # Errors like broken pipes (program which takes the standard # output terminates before this program terminates) should not # generate a traceback. if e.errno == EPIPE: exit(0) raise def cat(ppd): # Ignore driver's name, take only PPD's ppd = ppd.split(":")[-1] # Remove also the index ppd = "0/" + ppd[ppd.find("/")+1:] ppds = load() ppds['ARCHIVE'] = BytesIO(decompress(ppds['ARCHIVE'])) if ppd in ppds: start = ppds[ppd][0] length = ppds[ppd][1] ppds['ARCHIVE'].seek(start) return ppds['ARCHIVE'].read(length) def main(): usage = "usage: %prog list\n" \ " %prog cat URI" version = "%prog 1.0.2\n" \ "Copyright (c) 2013 Vitor Baptista.\n" \ "This is free software; see the source for copying conditions.\n" \ "There is NO warranty; not even for MERCHANTABILITY or\n" \ "FITNESS FOR A PARTICULAR PURPOSE." parser = OptionParser(usage=usage, version=version) (options, args) = parser.parse_args() if len(args) == 0 or len(args) > 2: parser.error("incorrect number of arguments") if args[0].lower() == 'list': ls() elif args[0].lower() == 'cat': if not len(args) == 2: parser.error("incorrect number of arguments") ppd = cat(args[1]) if not ppd: parser.error("Printer '%s' does not have default driver!" % args[1]) try: # avoid any assumption of encoding or system locale; just print the # bytes of the PPD as they are if sys.version_info.major < 3: sys.stdout.write(ppd) else: sys.stdout.buffer.write(ppd) except IOError as e: # Errors like broken pipes (program which takes the standard output # terminates before this program terminates) should not generate a # traceback. if e.errno == EPIPE: exit(0) raise else: parser.error("argument " + args[0] + " invalid") # PPDs Archive ppds_compressed_b64 = b"@ppds_compressed_b64@" if __name__ == "__main__": try: main() except KeyboardInterrupt: # We don't want a KeyboardInterrupt throwing a # traceback into stdout. pass pyppd-1.0.2/pyppd/runner.py0000664000175000017500000000507212214434520016220 0ustar vitorvitor00000000000000import os import stat import errno import logging import logging.handlers from optparse import OptionParser import pyppd.archiver def parse_args(): usage = "usage: %prog [options] ppds_directory" version = "%prog 1.0.2\n" \ "Copyright (c) 2013 Vitor Baptista.\n" \ "This is free software; see the source for copying conditions.\n" \ "There is NO warranty; not even for MERCHANTABILITY or\n" \ "FITNESS FOR A PARTICULAR PURPOSE." parser = OptionParser(usage=usage, version=version) parser.add_option("-v", "--verbose", action="count", dest="verbosity", help="run verbosely (can be supplied multiple times to " \ "increase verbosity)") parser.add_option("-d", "--debug", action="store_const", const=2, dest="verbose", help="print debug messages") parser.add_option("-o", "--output", default="pyppd-ppdfile", metavar="FILE", help="write archive to FILE [default %default]") (options, args) = parser.parse_args() if len(args) != 1: parser.error("incorrect number of arguments") if not os.path.isdir(args[0]): parser.error("'%s' isn't a directory" % args[0]) return (options, args) def configure_logging(verbosity): """Configures logging verbosity To stdout, we only WARNING of worse messages in a simpler format. To the file, we save every log message with its time, level, module and method. We also rotate the log_file, removing old entries when it reaches 2 MB. """ if verbosity == 1: level = logging.INFO elif verbosity == 2: level = logging.DEBUG else: level = logging.WARNING formatter = '[%(levelname)s] %(module)s.%(funcName)s(): %(message)s' logging.basicConfig(level=level, format=formatter) def run(): (options, args) = parse_args() configure_logging(options.verbosity) ppds_directory = args[0] logging.info('Archiving folder "%s".' % ppds_directory) archive = pyppd.archiver.archive(ppds_directory) if not archive: exit(errno.ENOENT) logging.info('Writing archive to "%s".' % options.output) output = open(options.output, "wb+") output.write(archive) output.close() logging.info('Setting "%s" executable flag.' % options.output) execute_mode = stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH mode = os.stat(options.output).st_mode | execute_mode os.chmod(options.output, mode) pyppd-1.0.2/pyppd/compressor.py0000664000175000017500000000123312212071452017075 0ustar vitorvitor00000000000000from subprocess import Popen, PIPE def compress(value): """Compresses a byte array with the xz binary""" process = Popen(["xz", "--compress", "--force"], stdin=PIPE, stdout=PIPE) return process.communicate(value)[0] def decompress(value): """Decompresses a byte array with the xz binary""" process = Popen(["xz", "--decompress", "--stdout", "--force"], stdin=PIPE, stdout=PIPE) return process.communicate(value)[0] def compress_file(path): """Compress the file at 'path' with the xz binary""" process = Popen(["xz", "--compress", "--force", "--stdout", path], stdout=PIPE) return process.communicate()[0] pyppd-1.0.2/pyppd/__init__.py0000664000175000017500000000000012211124032016417 0ustar vitorvitor00000000000000pyppd-1.0.2/ISSUES0000664000175000017500000000034312211124032014122 0ustar vitorvitor00000000000000Issues ------ * Add unit tests. * When testing if the PPD generated from the \*Product line already exists in a \*1284DeviceID line, we do a case-sensitive test. I think it should be case- insensitive. * Generate manpage. pyppd-1.0.2/setup.py0000775000175000017500000000232712214434511014716 0ustar vitorvitor00000000000000#!/usr/bin/env python from distutils.core import setup from distutils.command.sdist import sdist as _sdist class sdist(_sdist): def run(self): try: import sys sys.path.append("contrib") import git2changes print('generating CHANGES.txt') with open('CHANGES.txt', 'w+') as f: git2changes.run(f) except ImportError: pass _sdist.run(self) setup( name='pyppd', version='1.0.2', author='Vitor Baptista', author_email='vitor@vitorbaptista.com', packages=['pyppd'], package_data={'pyppd': ['*.in']}, scripts=['bin/pyppd'], url='http://github.com/vitorbaptista/pyppd/', license='MIT', description='A CUPS PostScript Printer Driver\'s compressor and generator', long_description=open('README', 'rb').read().decode('UTF-8') + "\n" + open('ISSUES', 'rb').read().decode('UTF-8'), cmdclass={'sdist': sdist}, classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: System Administrators', 'Operating System :: POSIX', 'License :: OSI Approved :: MIT License', 'Topic :: Printing', ], )