# # Copyright (c) 2016 - 2024 -- Lars Heuer # All rights reserved. # # License: BSD License # """\ Additional factory functions for common QR codes. Aside from :py:func:`make_epc_qr`, the factory functions return a QR code with the minimum error correction level "L" (or better). To create a (Micro) QR code which should use a specific error correction level or version etc., use the "_data" factory functions which return a string which can be used as input for :py:func:`segno.make()`. """ import re import decimal import segno from urllib.parse import quote _MECARD_ESCAPE = { ord('\\'): "\\\\", ord(';'): "\\;", ord(':'): "\\:", ord('"'): '\\"', } _VCARD_ESCAPE = { ord(','): '\\,', ord(';'): '\\;', } def _escape_mecard(s): """\ Escapes ``\\``, ``;``, ``"`` and ``:`` in the provided string. :param str s: The string to escape. :rtype str """ return str(s).translate(_MECARD_ESCAPE) def _escape_vcard(s): """\ Escapes ``\\``, ``;``, ``"`` and ``:`` in the provided string. :param str s: The string to escape. :rtype str """ return str(s).translate(_VCARD_ESCAPE) def make_wifi_data(ssid, password=None, security=None, hidden=False): """\ Creates WIFI configuration string. :param str ssid: The SSID of the network. :param password: The password. :type password: str or None :param security: Authentication type; the value should be "WEP" or "WPA". Set to ``None`` to omit the value. "nopass" is equivalent to setting the value to ``None`` but in the former case, the value is not omitted. :type security: str or None :param bool hidden: Indicates if the network is hidden (default: ``False``) :rtype: str """ escape = _escape_mecard data = 'WIFI:' if security: data += f'T:{security.upper() if security != "nopass" else security};' data += f'S:{escape(ssid)};' if password is not None: data += f'P:{escape(password)};' data += 'H:true;' if hidden else ';' return data def make_wifi(ssid, password=None, security=None, hidden=False): """\ Creates a WIFI configuration QR code. :param str ssid: The SSID of the network. :param password: The password. :type password: str or None :param security: Authentication type; the value should be "WEP" or "WPA". Set to ``None`` to omit the value. "nopass" is equivalent to setting the value to ``None`` but in the former case, the value is not omitted. :type security: str or None :param bool hidden: Indicates if the network is hidden (default: ``False``) :rtype: segno.QRCode """ return segno.make_qr(make_wifi_data(ssid, password, security, hidden)) def make_mecard_data(name, reading=None, email=None, phone=None, videophone=None, memo=None, nickname=None, birthday=None, url=None, pobox=None, roomno=None, houseno=None, city=None, prefecture=None, zipcode=None, country=None): """\ Creates a string encoding the contact information as MeCard. :param str name: Name. If it contains a comma, the first part is treated as lastname and the second part is treated as forename. :param reading: Designates a text string to be set as the kana name in the phonebook :type reading: str or None :param email: E-mail address. Multiple values are allowed. :type email: str, iterable of strings, or None :param phone: Phone number. Multiple values are allowed. :type phone: str, iterable of strings, or None :param videophone: Phone number for video calls. Multiple values are allowed. :type videophone: str, iterable of strings, or None :param memo: A notice for the contact. :type memo: str or None :param nickname: Nickname. :type nickname: str or None :param birthday: Birthday. If a string is provided, it should encode the date as YYYYMMDD value. :type birthday: str, datetime.date or None :param url: Homepage. Multiple values are allowed. :type url: str, iterable of strings, or None :param pobox: P.O. box (address information). :type pobox: str or None :param roomno: Room number (address information). :type roomno: str or None :param houseno: House number (address information). :type houseno: str or None :param city: City (address information). :type city: str or None :param prefecture: Prefecture (address information). :type prefecture: str or None :param zipcode: Zip code (address information). :type zipcode: str or None :param country: Country (address information). :type country: str or None :rtype: str """ def make_multifield(name, val): if not val: return () if isinstance(val, str): val = (val,) return [f'{name}:{escape(i)};' for i in val] escape = _escape_mecard data = [f'MECARD:N:{escape(name)};'] if reading: data.append(f'SOUND:{escape(reading)};') data.extend(make_multifield('TEL', phone)) data.extend(make_multifield('TELAV', videophone)) data.extend(make_multifield('EMAIL', email)) if nickname: data.append(f'NICKNAME:{escape(nickname)};') if birthday: try: birthday = birthday.strftime('%Y%m%d') except AttributeError: pass data.append(f'BDAY:{birthday};') data.extend(make_multifield('URL', url)) adr_properties = (pobox, roomno, houseno, city, prefecture, zipcode, country) if any(adr_properties): adr_data = [escape(i or '') for i in adr_properties] data.append('ADR:{0},{1},{2},{3},{4},{5},{6};'.format(*adr_data)) # noqa UP030 if memo: data.append(f'MEMO:{escape(memo)};') data.append(';') return ''.join(data) def make_mecard(name, reading=None, email=None, phone=None, videophone=None, memo=None, nickname=None, birthday=None, url=None, pobox=None, roomno=None, houseno=None, city=None, prefecture=None, zipcode=None, country=None): """\ Returns a QR code which encodes a `MeCard `_ :param str name: Name. If it contains a comma, the first part is treated as lastname and the second part is treated as forename. :param reading: Designates a text string to be set as the kana name in the phonebook :type reading: str or None :param email: E-mail address. Multiple values are allowed. :type email: str, iterable of strings, or None :param phone: Phone number. Multiple values are allowed. :type phone: str, iterable of strings, or None :param videophone: Phone number for video calls. Multiple values are allowed. :type videophone: str, iterable of strings, or None :param memo: A notice for the contact. :type memo: str or None :param nickname: Nickname. :type nickname: str or None :param birthday: Birthday. If a string is provided, it should encode the date as YYYYMMDD value. :type birthday: str, datetime.date or None :param url: Homepage. Multiple values are allowed. :type url: str, iterable of strings, or None :param pobox: P.O. box (address information). :type pobox: str or None :param roomno: Room number (address information). :type roomno: str or None :param houseno: House number (address information). :type houseno: str or None :param city: City (address information). :type city: str or None :param prefecture: Prefecture (address information). :type prefecture: str or None :param zipcode: Zip code (address information). :type zipcode: str or None :param country: Country (address information). :type country: str or None :rtype: segno.QRCode """ return segno.make_qr(make_mecard_data(name=name, reading=reading, email=email, phone=phone, videophone=videophone, memo=memo, nickname=nickname, birthday=birthday, url=url, pobox=pobox, roomno=roomno, houseno=houseno, city=city, prefecture=prefecture, zipcode=zipcode, country=country)) _looks_like_datetime = re.compile(r'^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2}(?:(?:-?\d{2}:\d{2})|Z)?)?$').match def make_vcard_data(name, displayname, email=None, phone=None, fax=None, videophone=None, memo=None, nickname=None, birthday=None, url=None, pobox=None, street=None, city=None, region=None, zipcode=None, country=None, org=None, lat=None, lng=None, source=None, rev=None, title=None, photo_uri=None, cellphone=None, homephone=None, workphone=None): """\ Creates a string encoding the contact information as vCard 3.0. Only a subset of available `vCard 3.0 properties ` is supported. :param str name: The name. If it contains a semicolon, , the first part is treated as lastname and the second part is treated as forename. :param str displayname: Common name. :param email: E-mail address. Multiple values are allowed. :type email: str, iterable of strings, or None :param phone: Phone number. Multiple values are allowed. :type phone: str, iterable of strings, or None :param fax: Fax number. Multiple values are allowed. :type fax: str, iterable of strings, or None :param videophone: Phone number for video calls. Multiple values are allowed. :type videophone: str, iterable of strings, or None :param memo: A notice for the contact. :type memo: str or None :param nickname: Nickname. :type nickname: str or None :param birthday: Birthday. If a string is provided, it should encode the date as ``YYYY-MM-DD`` value. :type birthday: str, datetime.date or None :param url: Homepage. Multiple values are allowed. :type url: str, iterable of strings, or None :param pobox: P.O. box (address information). :type pobox: str or None :param street: Street address. :type street: str or None :param city: City (address information). :type city: str or None :param region: Region (address information). :type region: str or None :param zipcode: Zip code (address information). :type zipcode: str or None :param country: Country (address information). :type country: str or None :param org: Company / organization name. :type org: str or None :param lat: Latitude. :type lat: float or None :param lng: Longitude. :type lng: float or None :param source: URL where to obtain the vCard. :type source: str or None :param rev: Revision of the vCard / last modification date. :type rev: str, datetime.date or None :param title: Job Title. Multiple values are allowed. :type title: str, iterable of strings, or None :param photo_uri: Photo URI. Multiple values are allowed. :type photo_uri: str, iterable of strings, or None :param cellphone: Cell phone number. Multiple values are allowed. :type cellphone: str, iterable of strings, or None :param homephone: Home phone number. Multiple values are allowed. :type homephone: str, iterable of strings, or None :param workphone: Work phone number. Multiple values are allowed. :type workphone: str, iterable of strings, or None :rtype: str """ def make_multifield(name, val): if not val: return () if isinstance(val, str): val = (val,) return [f'{name}:{escape(i)}' for i in val] escape = _escape_vcard data = ['BEGIN:VCARD', 'VERSION:3.0', f'N:{name}', f'FN:{escape(displayname)}'] if org: data.append(f'ORG:{escape(org)}') data.extend(make_multifield('EMAIL', email)) data.extend(make_multifield('TEL', phone)) data.extend(make_multifield('TEL;TYPE=FAX', fax)) data.extend(make_multifield('TEL;TYPE=VIDEO', videophone)) data.extend(make_multifield('TEL;TYPE=CELL', cellphone)) data.extend(make_multifield('TEL;TYPE=HOME', homephone)) data.extend(make_multifield('TEL;TYPE=WORK', workphone)) data.extend(make_multifield('URL', url)) data.extend(make_multifield('TITLE', title)) data.extend(make_multifield('PHOTO;VALUE=uri', photo_uri)) if nickname: data.append(f'NICKNAME:{escape(nickname)}') adr_properties = (pobox, street, city, region, zipcode, country) if any(adr_properties): adr_data = [escape(i or '') for i in adr_properties] data.append('ADR:{0};;{1};{2};{3};{4};{5}'.format(*adr_data)) # noqa UP030 if birthday: try: birthday = birthday.strftime('%Y-%m-%d') except AttributeError: pass if not isinstance(birthday, str) or not _looks_like_datetime(birthday): raise ValueError('"birthday" does not seem to be a valid date or date/time representation') data.append(f'BDAY:{birthday}') if (lat and not lng) or (lng and not lat): raise ValueError('Incomplete geo information, please specify latitude and longitude.') if lat and lng: data.append(f'GEO:{lat};{lng}') if source: data.append(f'SOURCE:{escape(source)}') if memo: data.append(f'NOTE:{escape(memo)}') if rev: try: rev = rev.strftime('%Y-%m-%d') except AttributeError: pass if not isinstance(rev, str) or not _looks_like_datetime(rev): raise ValueError('"rev" does not seem to be a valid date or date/time representation') data.append(f'REV:{rev}') data.append('END:VCARD') data.append('') return '\r\n'.join(data) def make_vcard(name, displayname, email=None, phone=None, fax=None, videophone=None, memo=None, nickname=None, birthday=None, url=None, pobox=None, street=None, city=None, region=None, zipcode=None, country=None, org=None, lat=None, lng=None, source=None, rev=None, title=None, photo_uri=None, cellphone=None, homephone=None, workphone=None): """\ Creates a QR code which encodes a `vCard `_ version 3.0. Only a subset of available `vCard 3.0 properties ` is supported. :param str name: The name. If it contains a semicolon, , the first part is treated as lastname and the second part is treated as forename. :param str displayname: Common name. :param email: E-mail address. Multiple values are allowed. :type email: str, iterable of strings, or None :param phone: Phone number. Multiple values are allowed. :type phone: str, iterable of strings, or None :param fax: Fax number. Multiple values are allowed. :type fax: str, iterable of strings, or None :param videophone: Phone number for video calls. Multiple values are allowed. :type videophone: str, iterable of strings, or None :param memo: A notice for the contact. :type memo: str or None :param nickname: Nickname. :type nickname: str or None :param birthday: Birthday. If a string is provided, it should encode the date as ``YYYY-MM-DD`` value. :type birthday: str, datetime.date or None :param url: Homepage. Multiple values are allowed. :type url: str, iterable of strings, or None :param pobox: P.O. box (address information). :type pobox: str or None :param street: Street address. :type street: str or None :param city: City (address information). :type city: str or None :param region: Region (address information). :type region: str or None :param zipcode: Zip code (address information). :type zipcode: str or None :param country: Country (address information). :type country: str or None :param org: Company / organization name. :type org: str or None :param lat: Latitude. :type lat: float or None :param lng: Longitude. :type lng: float or None :param source: URL where to obtain the vCard. :type source: str or None :param rev: Revision of the vCard / last modification date. :type rev: str, datetime.date or None :param title: Job Title. Multiple values are allowed. :type title: str, iterable of strings, or None :param photo_uri: Photo URI. Multiple values are allowed. :type photo_uri: str, iterable of strings, or None :param cellphone: Cell phone number. Multiple values are allowed. :type cellphone: str, iterable of strings, or None :param homephone: Home phone number. Multiple values are allowed. :type homephone: str, iterable of strings, or None :param workphone: Work phone number. Multiple values are allowed. :type workphone: str, iterable of strings, or None :rtype: segno.QRCode """ return segno.make_qr(make_vcard_data(name, displayname, email=email, phone=phone, fax=fax, videophone=videophone, memo=memo, nickname=nickname, birthday=birthday, url=url, pobox=pobox, street=street, city=city, region=region, zipcode=zipcode, country=country, org=org, lat=lat, lng=lng, source=source, rev=rev, title=title, photo_uri=photo_uri, cellphone=cellphone, homephone=homephone, workphone=workphone)) def make_geo_data(lat, lng): """\ Creates a geo location URI. :param float lat: Latitude :param float lng: Longitude :rtype: str """ def float_to_str(f): return f'{f:.8f}'.rstrip('0').rstrip('.') return f'geo:{float_to_str(lat)},{float_to_str(lng)}' def make_geo(lat, lng): """\ Returns a QR code which encodes geographic location using the ``geo`` URI scheme. :param float lat: Latitude :param float lng: Longitude :rtype: segno.QRCode """ return segno.make_qr(make_geo_data(lat, lng)) def make_make_email_data(to, cc=None, bcc=None, subject=None, body=None): """\ Creates either a simple "mailto:" URL or complete e-mail message with (blind) carbon copies and a subject and a body. :param to: The email address (recipient). Multiple values are allowed. :type to: str or iterable of strings :param cc: The carbon copy recipient. Multiple values are allowed. :type cc: str, iterable of strings, or None :param bcc: The blind carbon copy recipient. Multiple values are allowed. :type bcc: str, iterable of strings, or None :param subject: The subject. :type subject: str or None :param body: The message body. :type body: str or None :rtype: str """ def multi(val): if not val: return () if isinstance(val, str): return (val,) return tuple(val) delim = '?' data = ['mailto:'] if not to: raise ValueError('"to" must not be empty or None') data.append(','.join(multi(to))) for key, val in (('cc', cc), ('bcc', bcc)): vals = multi(val) if vals: data.append(f'{delim}{key}={",".join(vals)}') delim = '&' for key, val in (('subject', subject), ('body', body)): if val is not None: data.append(f'{delim}{key}={quote(val.encode("utf-8"))}') delim = '&' return ''.join(data) def make_email(to, cc=None, bcc=None, subject=None, body=None): """\ Encodes either a simple e-mail address or a complete message with (blind) carbon copies and a subject and a body. :param to: The email address (recipient). Multiple values are allowed. :type to: str or iterable of strings :param cc: The carbon copy recipient. Multiple values are allowed. :type cc: str, iterable of strings, or None :param bcc: The blind carbon copy recipient. Multiple values are allowed. :type bcc: str, iterable of strings, or None :param subject: The subject. :type subject: str or None :param body: The message body. :type body: str or None :rtype: segno.QRCode """ return segno.make_qr(make_make_email_data(to=to, cc=cc, bcc=bcc, subject=subject, body=body)) def _make_epc_qr_data(name, iban, amount, text=None, reference=None, bic=None, purpose=None, encoding=None): """\ Validates the input and creates the data for an EPC QR Code. DOES NOT belong to the public API, kept separate from make_epc_qr to apply tests on the raw data. See :py:func:`make_epc_qr` for a description of the parameters. """ # Ordering is important! encodings = ('utf-8', 'iso-8859-1', 'iso-8859-2', 'iso-8859-4', 'iso-8859-5', 'iso-8859-7', 'iso-8859-10', 'iso-8859-15') min_amount = decimal.Decimal('0.01') max_amount = decimal.Decimal('999999999.99') text = text.rstrip() if text else text reference = reference.rstrip() if reference else reference bic = bic.strip() if bic else bic name = name.strip() if name else name if encoding is not None: if isinstance(encoding, str): try: encoding = encodings.index(encoding.lower()) + 1 except ValueError: raise ValueError(f'Invalid encoding "{encoding}", use one of {encodings}') elif not isinstance(encoding, int) or not 1 <= encoding <= len(encodings): raise ValueError(f'Invalid encoding number only 1 .. 8 are allowed, got "{encoding}"') if (not text and not reference) or (text and reference): raise ValueError('Either a text or a creditor reference (ISO 11649) must be provided') if text and not 0 < len(text) <= 140: raise ValueError(f'Invalid text, max. 140 characters are allowed, got "{len(text)}"') elif reference and not 0 < len(reference) <= 35: raise ValueError('Invalid creditor reference (ISO 11649), max. 35 characters are allowed, ' f'got "{len(reference)}"') if name is None or not 0 < len(name) <= 70: raise ValueError(f'Invalid name, max. 70 characters are allowed, got "{name}"') if iban is None or not 4 < len(iban) <= 34: raise ValueError(f'Invalid IBAN, min. 5 and max. 34 characters are allowed, got "{iban}"') if bic and len(bic) not in (8, 11): raise ValueError(f'Invalid BIC, should be 8 or 11 characters long, got "{bic}"') if purpose and len(purpose) != 4: raise ValueError(f'Invalid purpose, 4 characters are allowed, got "{purpose}"') amount = decimal.Decimal(amount) if not min_amount <= amount <= max_amount: raise ValueError(f'Invalid amount, must be in bigger or equal {min_amount} and less or equal {max_amount}') tmp_data = ['BCD', # Service tag '002', # Version '', # character set (will be set later) 'SCT', # Identification bic or '', # BIC name, # Name of the recipient iban, # IBAN f'EUR{amount:.2f}'.rstrip('0').rstrip('.'), # Amount purpose or '', # Purpose reference or '', # Remittance ] if text: tmp_data.append(text) data = '\n'.join(tmp_data) charset = -1 if encoding is None else encoding if charset < 0: for idx, enc in enumerate(encodings[1:], start=2): try: data.encode(enc) charset = idx break except UnicodeEncodeError: pass if charset < 0: charset = 1 # Use UTF-8 tmp_data[2] = str(charset) # Set character set data = '\n'.join(tmp_data).encode(encodings[charset - 1]) # Max. payload: 331 bytes if len(data) > 331: # pragma: no cover raise ValueError(f'Payload is too big: Max. 331 bytes allowed, got {len(data)} bytes') return data def make_epc_qr(name, iban, amount, text=None, reference=None, bic=None, purpose=None, encoding=None): """\ Creates and returns an European Payments Council Quick Response Code (EPC QR Code) version 002. The returned :py:class:`segno.QRCode` uses always the error correction level "M" and utilizes max. version 13 to fulfill the constraints of the EPC QR Code standard. .. note:: Either the ``text`` or ``reference`` must be provided but not both .. note:: Neither the IBAN, BIC, nor remittance reference number or any other information is validated (aside from checks regarding the allowed string lengths). :param str name: Name of the recipient. :param str iban: International Bank Account Number (IBAN) :param amount: The amount (in EUR) to transfer. The currency is always Euro, no other currencies are supported. :type amount: int, float, decimal.Decimal :param str text: Remittance Information (unstructured) :param str reference: Remittance Information (structured) :param str bic: Bank Identifier Code (BIC). Optional, only required for non-EEA countries. :param str purpose: SEPA purpose code. :param encoding: By default, this function tries to find the best, minimal encoding. If another encoding should be used, the encoding name or the encoding constant (an integer) can be provided: ``1``: "UTF-8", ``2``: "ISO 8859-1", ``3``: "ISO 8859-2", ``4``: "ISO 8859-4", ``5``: "ISO 8859-5", ``6``: "ISO 8859-7", ``7``: "ISO 8859-10", ``8``: "ISO 8859-15" The encoding is case-insensitive. :type encoding: str or int :rtype: segno.QRCode """ # Create a QR Code, error correction level "M". # It's not allowed to use another level therefore boost_error must be disabled qr = segno.make_qr(_make_epc_qr_data(name, iban, amount, text, reference, bic, purpose, encoding), error='m', boost_error=False) # This shouldn't happen if qr.version > 13: # pragma: no cover raise ValueError(f'Invalid EPC QR Code, max. QR Code version 13 is allowed, got "{qr.designator}"') return qr