console_scripts.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. #!/usr/bin/env python
  2. """
  3. qr - Convert stdin (or the first argument) to a QR Code.
  4. When stdout is a tty the QR Code is printed to the terminal and when stdout is
  5. a pipe to a file an image is written. The default image format is PNG.
  6. """
  7. import optparse
  8. import os
  9. import sys
  10. from typing import NoReturn, Optional
  11. from collections.abc import Iterable
  12. from importlib import metadata
  13. import qrcode
  14. from qrcode.image.base import BaseImage, DrawerAliases
  15. # The next block is added to get the terminal to display properly on MS platforms
  16. if sys.platform.startswith(("win", "cygwin")): # pragma: no cover
  17. import colorama # type: ignore
  18. colorama.init()
  19. default_factories = {
  20. "pil": "qrcode.image.pil.PilImage",
  21. "png": "qrcode.image.pure.PyPNGImage",
  22. "svg": "qrcode.image.svg.SvgImage",
  23. "svg-fragment": "qrcode.image.svg.SvgFragmentImage",
  24. "svg-path": "qrcode.image.svg.SvgPathImage",
  25. # Keeping for backwards compatibility:
  26. "pymaging": "qrcode.image.pure.PymagingImage",
  27. }
  28. error_correction = {
  29. "L": qrcode.ERROR_CORRECT_L,
  30. "M": qrcode.ERROR_CORRECT_M,
  31. "Q": qrcode.ERROR_CORRECT_Q,
  32. "H": qrcode.ERROR_CORRECT_H,
  33. }
  34. def main(args=None):
  35. if args is None:
  36. args = sys.argv[1:]
  37. version = metadata.version("qrcode")
  38. parser = optparse.OptionParser(usage=(__doc__ or "").strip(), version=version)
  39. # Wrap parser.error in a typed NoReturn method for better typing.
  40. def raise_error(msg: str) -> NoReturn:
  41. parser.error(msg)
  42. raise # pragma: no cover
  43. parser.add_option(
  44. "--factory",
  45. help="Full python path to the image factory class to "
  46. "create the image with. You can use the following shortcuts to the "
  47. f"built-in image factory classes: {commas(default_factories)}.",
  48. )
  49. parser.add_option(
  50. "--factory-drawer",
  51. help=f"Use an alternate drawer. {get_drawer_help()}.",
  52. )
  53. parser.add_option(
  54. "--optimize",
  55. type=int,
  56. help="Optimize the data by looking for chunks "
  57. "of at least this many characters that could use a more efficient "
  58. "encoding method. Use 0 to turn off chunk optimization.",
  59. )
  60. parser.add_option(
  61. "--error-correction",
  62. type="choice",
  63. choices=sorted(error_correction.keys()),
  64. default="M",
  65. help="The error correction level to use. Choices are L (7%), "
  66. "M (15%, default), Q (25%), and H (30%).",
  67. )
  68. parser.add_option(
  69. "--ascii", help="Print as ascii even if stdout is piped.", action="store_true"
  70. )
  71. parser.add_option(
  72. "--output",
  73. help="The output file. If not specified, the image is sent to "
  74. "the standard output.",
  75. )
  76. opts, args = parser.parse_args(args)
  77. if opts.factory:
  78. module = default_factories.get(opts.factory, opts.factory)
  79. try:
  80. image_factory = get_factory(module)
  81. except ValueError as e:
  82. raise_error(str(e))
  83. else:
  84. image_factory = None
  85. qr = qrcode.QRCode(
  86. error_correction=error_correction[opts.error_correction],
  87. image_factory=image_factory,
  88. )
  89. if args:
  90. data = args[0]
  91. data = data.encode(errors="surrogateescape")
  92. else:
  93. data = sys.stdin.buffer.read()
  94. if opts.optimize is None:
  95. qr.add_data(data)
  96. else:
  97. qr.add_data(data, optimize=opts.optimize)
  98. if opts.output:
  99. img = qr.make_image()
  100. with open(opts.output, "wb") as out:
  101. img.save(out)
  102. else:
  103. if image_factory is None and (os.isatty(sys.stdout.fileno()) or opts.ascii):
  104. qr.print_ascii(tty=not opts.ascii)
  105. return
  106. kwargs = {}
  107. aliases: Optional[DrawerAliases] = getattr(
  108. qr.image_factory, "drawer_aliases", None
  109. )
  110. if opts.factory_drawer:
  111. if not aliases:
  112. raise_error("The selected factory has no drawer aliases.")
  113. if opts.factory_drawer not in aliases:
  114. raise_error(
  115. f"{opts.factory_drawer} factory drawer not found."
  116. f" Expected {commas(aliases)}"
  117. )
  118. drawer_cls, drawer_kwargs = aliases[opts.factory_drawer]
  119. kwargs["module_drawer"] = drawer_cls(**drawer_kwargs)
  120. img = qr.make_image(**kwargs)
  121. sys.stdout.flush()
  122. img.save(sys.stdout.buffer)
  123. def get_factory(module: str) -> type[BaseImage]:
  124. if "." not in module:
  125. raise ValueError("The image factory is not a full python path")
  126. module, name = module.rsplit(".", 1)
  127. imp = __import__(module, {}, {}, [name])
  128. return getattr(imp, name)
  129. def get_drawer_help() -> str:
  130. help: dict[str, set] = {}
  131. for alias, module in default_factories.items():
  132. try:
  133. image = get_factory(module)
  134. except ImportError: # pragma: no cover
  135. continue
  136. aliases: Optional[DrawerAliases] = getattr(image, "drawer_aliases", None)
  137. if not aliases:
  138. continue
  139. factories = help.setdefault(commas(aliases), set())
  140. factories.add(alias)
  141. return ". ".join(
  142. f"For {commas(factories, 'and')}, use: {aliases}"
  143. for aliases, factories in help.items()
  144. )
  145. def commas(items: Iterable[str], joiner="or") -> str:
  146. items = tuple(items)
  147. if not items:
  148. return ""
  149. if len(items) == 1:
  150. return items[0]
  151. return f"{', '.join(items[:-1])} {joiner} {items[-1]}"
  152. if __name__ == "__main__": # pragma: no cover
  153. main()