├── civic.json ├── static ├── images │ ├── c4sj_logo.jpg │ └── nametag_web.gif ├── styles │ ├── reset.css │ └── main.css ├── index.html └── js │ ├── nametag.js │ └── qrcode.js ├── config.py ├── client_secret.json.example ├── .gitignore ├── install ├── autostart └── install.sh ├── requirements.txt ├── templates └── thankyou.html ├── LICENSE ├── main.py ├── logging.json ├── nametag_server.py ├── README.md └── uploader.py /civic.json: -------------------------------------------------------------------------------- 1 | { 2 | "status": "Alpja", 3 | "tags": ["login", "data", "registration", "automation"] 4 | } 5 | 6 | -------------------------------------------------------------------------------- /static/images/c4sj_logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforsanjose/BadgeHub/HEAD/static/images/c4sj_logo.jpg -------------------------------------------------------------------------------- /static/images/nametag_web.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeforsanjose/BadgeHub/HEAD/static/images/nametag_web.gif -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | CSV_FILENAME = "userInformation.csv" 2 | SPREADSHEET_ID = '1e6PV5ejmVUXSXiHPJR033pR1d-wKvLR8G9xWr_n4wtc' 3 | LOGGING_ID = "cfsj_logger" -------------------------------------------------------------------------------- /client_secret.json.example: -------------------------------------------------------------------------------- 1 | Turn on the Google Sheets API: https://developers.google.com/sheets/api/quickstart/python 2 | 3 | For more information, go to README.md. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.swp 3 | venv/ 4 | 5 | # output from the program 6 | printNameBadge.txt 7 | userInformation.csv 8 | temp.png 9 | client_secret.json 10 | *.log -------------------------------------------------------------------------------- /install/autostart: -------------------------------------------------------------------------------- 1 | @lxpanel --profile LXDE-pi 2 | @pcmanfm --desktop --profile LXDE-pi 3 | #@xscreensaver -no-splash # comment this line out to disable screensaver 4 | @xset s off 5 | @xset -dpms 6 | @xset s noblank 7 | @point-rpi 8 | @python /home/pi/GitHub/CFSJ-Login-System/nametag_server.py 9 | @chromium-browser --incognito --kiosk http://localhost:5000/ 10 | 11 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | apiclient==1.0.3 2 | appdirs==1.4.2 3 | cachetools==2.1.0 4 | click==6.7 5 | Flask==0.12 6 | google-api-python-client==1.7.4 7 | google-auth==1.5.1 8 | google-auth-httplib2==0.0.3 9 | httplib2==0.11.3 10 | itsdangerous==0.24 11 | Jinja2==2.9.5 12 | MarkupSafe==0.23 13 | oauth2client==4.1.2 14 | packaging==16.8 15 | pathlib==1.0.1 16 | pyasn1==0.4.4 17 | pyasn1-modules==0.2.2 18 | pyparsing==2.1.10 19 | rsa==3.4.2 20 | six==1.10.0 21 | uritemplate==3.0.0 22 | urllib3==1.23 23 | Werkzeug==0.11.15 24 | -------------------------------------------------------------------------------- /templates/thankyou.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | title 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |

16 |

Thank you!

17 |
18 |

{{ message }}

19 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Code for San Jose 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /static/styles/reset.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin: 0; 20 | padding: 0; 21 | border: 0; 22 | font-size: 100%; 23 | font: inherit; 24 | vertical-align: baseline; 25 | } 26 | /* HTML5 display-role reset for older browsers */ 27 | article, aside, details, figcaption, figure, 28 | footer, header, hgroup, menu, nav, section { 29 | display: block; 30 | } 31 | body { 32 | line-height: 1; 33 | } 34 | ol, ul { 35 | list-style: none; 36 | } 37 | blockquote, q { 38 | quotes: none; 39 | } 40 | blockquote:before, blockquote:after, 41 | q:before, q:after { 42 | content: ''; 43 | content: none; 44 | } 45 | table { 46 | border-collapse: collapse; 47 | border-spacing: 0; 48 | } -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import os, multiprocessing 2 | from multiprocessing import Process 3 | import json 4 | import logging 5 | import logging.config 6 | from uploader import main 7 | from nametag_server import start_webserver 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | def sheets_uploader(): 12 | logger.info('Starting the uploader...{0}'.format(os.getpid())) 13 | main() 14 | 15 | def login_server(): 16 | logger.info('Starting the web server...{0}'.format(os.getpid())) 17 | start_webserver() 18 | 19 | def parent(): 20 | multiprocessing.freeze_support() 21 | p1 = Process(target = sheets_uploader, args = ()) 22 | p2 = Process(target = login_server, args = ()) 23 | p1.start() 24 | p2.start() 25 | 26 | def setup_logging( default_path='logging.json', default_level=logging.INFO, env_key='LOG_CFG'): 27 | """ 28 | Setup logging configuration 29 | see https://fangpenlin.com/posts/2012/08/26/good-logging-practice-in-python/ 30 | """ 31 | path = default_path 32 | value = os.getenv(env_key, None) 33 | if value: 34 | path = value 35 | if os.path.exists(path): 36 | with open(path, 'rt') as f: 37 | config = json.load(f) 38 | logging.config.dictConfig(config) 39 | else: 40 | logging.basicConfig(level=default_level) 41 | 42 | if __name__ == "__main__": 43 | setup_logging() 44 | parent() -------------------------------------------------------------------------------- /logging.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "disable_existing_loggers": false, 4 | "formatters": { 5 | "simple": { 6 | "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s" 7 | } 8 | }, 9 | 10 | "handlers": { 11 | "console": { 12 | "class": "logging.StreamHandler", 13 | "level": "DEBUG", 14 | "formatter": "simple", 15 | "stream": "ext://sys.stdout" 16 | }, 17 | 18 | "info_file_handler": { 19 | "class": "logging.handlers.RotatingFileHandler", 20 | "level": "INFO", 21 | "formatter": "simple", 22 | "filename": "info.log", 23 | "maxBytes": 10485760, 24 | "backupCount": 20, 25 | "encoding": "utf8" 26 | }, 27 | 28 | "error_file_handler": { 29 | "class": "logging.handlers.RotatingFileHandler", 30 | "level": "ERROR", 31 | "formatter": "simple", 32 | "filename": "errors.log", 33 | "maxBytes": 10485760, 34 | "backupCount": 20, 35 | "encoding": "utf8" 36 | } 37 | }, 38 | 39 | "loggers": { 40 | "my_module": { 41 | "level": "ERROR", 42 | "handlers": ["console"], 43 | "propagate": "no" 44 | } 45 | }, 46 | 47 | "root": { 48 | "level": "INFO", 49 | "handlers": ["console", "info_file_handler", "error_file_handler"] 50 | } 51 | } -------------------------------------------------------------------------------- /static/styles/main.css: -------------------------------------------------------------------------------- 1 | 2 | html, body {position:absolute;top:0;left:0;display: block;width: 800px; 3 | height: 480px; 4 | margin:0;padding:0;overflow: hidden;} 5 | body, h1, h2, h3, h4, p {font-family: sans-serif;} 6 | h1 {font-size:3em;} 7 | h2{font-size: 1.2em; margin-top: .4rem; margin-bottom: .6rem;} 8 | 9 | h1, h1 img {display: inline-block;height:8rem;} 10 | .header {display: flex; position:relative; top:0; left:0;align-items: center; justify-content: center;float: left;width: 100%;height:8rem;margin:1rem;} 11 | .header h1 { position: absolute; left: 0;margin: 1rem;} 12 | .header p {display: inline-block; font-size: 2.5em; line-height: 2.5em;} 13 | 14 | form {display: inline-block; float: left; padding: 1.5rem; box-sizing: border-box; top: 27px; width: 50%; font-size: 1.1em; line-height: 1.5em; position: relative;} 15 | form label {display:flex; justify-content: space-between; margin:1rem;font-size:1em;line-height: 1.5rem;} 16 | form label span {width:auto; text-align:left;} 17 | form label input { 18 | font-size: 1.0em; 19 | line-height: 1.5em; 20 | padding: 3px; 21 | border: 1px solid #a9a9a9; 22 | height: 1.5rem; 23 | width: 11.5rem;} 24 | form button {display:inline-block;float: right; margin: 1rem 1rem;font-size: 1em;width: 11em;} 25 | 26 | 27 | .nametag_preview {display:inline-block;width: 50%;float:right; padding: 2rem; box-sizing: border-box;} 28 | .nametag_preview.hidden {display:none;} 29 | #nametag_preview_surrogate {display:inline-block; border: 1px solid #3c3c3c; width: 100%;} 30 | 31 | .thankyou_text {display: block; text-align:center;font-size: 2.3em; line-height: 2.5em;} 32 | -------------------------------------------------------------------------------- /install/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ######################################################### 4 | # Install script for the # 5 | # Code For San Jose # 6 | # Login System # 7 | # https://github.com/codeforsanjose/CFSJ-Login-System # 8 | # # 9 | # (run this file once) # 10 | ######################################################### 11 | 12 | 13 | PROJECT_DIR=`pwd` 14 | 15 | sudo apt-get update 16 | sudo apt-get upgrade -y 17 | sudo apt-get install -y --force-yes \ 18 | zbar-tools \ 19 | qrencode \ 20 | libcups2-dev \ 21 | libcupsimage2-dev \ 22 | g++ \ 23 | cups \ 24 | cups-bsd \ 25 | cups-client 26 | 27 | 28 | # install the printer driver for the Dymo LabelWriter 450 29 | # for information about this, see the documentation on the 30 | # dymo website for the Linux SDK and CUPS Drivers 31 | cd /tmp 32 | wget http://download.dymo.com/Download%20Drivers/Linux/Download/dymo-cups-drivers-1.4.0.tar.gz 33 | tar xvf dymo-cups-drivers-1.4.0.tar.gz 34 | cd dymo-cups-drivers-1.4.0.5 35 | sudo ./configure 36 | sudo make 37 | sudo make install 38 | cd /tmp 39 | sudo rm -rf dymo-cups-drivers-1.4.0* 40 | sudo usermod -a -G lpadmin pi 41 | cd $PROJECT_DIR 42 | 43 | # install Flask and other Python dependencies 44 | sudo pip install -r requirements.txt 45 | 46 | 47 | # copy the startup scripts to allow Chromium and the login 48 | # system to start automatically at boot 49 | PI_CONFIG_DIR="/home/pi/.config/lxsession/LXDE-pi" 50 | if [ ! -f $PI_CONFIG_DIR"/autostart" ]; then 51 | cp $PI_CONFIG_DIR"/autostart" $PI_CONFIG_DIR"/autostart.orig" 52 | fi 53 | 54 | cp ./install/autostart $PI_CONFIG_DIR"/autostart" 55 | 56 | 57 | echo "Finished installing!" 58 | echo "Please set up your printer now at http://localhost:631" 59 | exit 0 -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | title 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |

16 |

Welcome! Please sign in.

17 |
18 |
19 |
20 | 22 |
23 | 24 | 28 | 32 |

Preferred Pronoun:

33 |
34 | 35 | 36 |
37 |
38 | 39 | 40 |
41 |
42 | 43 | 44 |
45 |
46 | 47 | 48 |
49 |
50 | 51 | 52 | 53 |
54 |
55 | 56 | 57 |
58 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /nametag_server.py: -------------------------------------------------------------------------------- 1 | import os, sys, base64, csv, datetime, logging 2 | from config import CSV_FILENAME, LOGGING_ID 3 | from flask import Flask, request, redirect, url_for, send_from_directory, render_template 4 | from werkzeug.utils import secure_filename 5 | 6 | PAGE_SIZE = "Custom.54x100mm" 7 | IMAGE_FILE = "temp.png" 8 | ALLOWED_EXTENSIONS = set(['txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif']) 9 | 10 | app = Flask(__name__) 11 | logger = logging.getLogger(LOGGING_ID) 12 | 13 | def get_script_path(): 14 | """ 15 | http://stackoverflow.com/a/4943474 16 | """ 17 | return os.path.dirname(os.path.realpath(sys.argv[0])) 18 | 19 | def allowed_file(filename): 20 | return '.' in filename and \ 21 | filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS 22 | 23 | def save_user_info(name, pronoun, email): 24 | needs_header = False 25 | csv_file = os.path.join(os.sep, get_script_path(), CSV_FILENAME) 26 | logger.info("using CSV file at {}".format(csv_file)) 27 | if not os.path.exists(csv_file): 28 | needs_header = True 29 | 30 | with open(csv_file, 'a') as csvfile: 31 | fieldnames = ["Name", "Pronoun", "Email", "Timestamp"] 32 | writer = csv.DictWriter(csvfile, fieldnames=fieldnames) 33 | if needs_header: 34 | writer.writeheader() 35 | writer.writerow({"Name":name, "Pronoun":pronoun, "Email":email, "Timestamp":datetime.datetime.now()}) 36 | 37 | def send_to_printer(): 38 | logger.info("sending image to printer") 39 | img_file = os.path.join(os.sep, get_script_path(), IMAGE_FILE) 40 | os.system("lpr -o landscape -o PageSize={} -o fit-to-page {}".format(PAGE_SIZE, img_file)) 41 | 42 | @app.route('/') 43 | def root(): 44 | return app.send_static_file('index.html') 45 | 46 | @app.route('/signin', methods=['POST']) 47 | def signin(): 48 | if request.method == 'POST': 49 | logger.info("'name = '{}' pronoun = '{}' email = '{}' nametag_img = '{}'".format( str(request.form['name']), str(request.form['pronoun']), str(request.form['email']), str(request.form['nametag_img']) ) ) 50 | img_file = os.path.join(os.sep, get_script_path(), IMAGE_FILE) 51 | logger.info("saving temp image at {}".format(img_file)) 52 | with open(img_file, "wb") as f: 53 | # Removing the prefix 'data:image/png;base64,' 54 | data = request.form['nametag_img'].split(",")[1] 55 | f.write(base64.b64decode(data)) 56 | save_user_info(request.form['name'], request.form['pronoun'], request.form['email']) 57 | 58 | # print only if the submit button value is "print" 59 | if request.form['button'] == "print": 60 | logger.info("Printing nametag for \"%s\""%request.form['name']) 61 | send_to_printer() 62 | return render_template("thankyou.html", message="Your nametag will print soon.") 63 | 64 | # otherwise simply submit 65 | elif request.form['button'] == "noprint": 66 | logger.info("Not printing for \"{}\"".format(request.form['name'])) 67 | return render_template("thankyou.html", message="Successfully signed in, enjoy your hack night!") 68 | 69 | 70 | def start_webserver(): 71 | app.run(host= '0.0.0.0') 72 | 73 | if __name__ == "__main__": 74 | start_webserver() 75 | 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Code For San Jose Login System 2 | 3 | The **Code For San Jose Login System** prints out name tags which contain the guest's name and email address as a QR code, name in plain text, and the Code For San Jose Brigade logo. The guest information is recorded in a `.csv` file and automatically uploaded to Google sheets when the Sheets API is turned on. 4 | 5 | 6 | ![creating a name tag](static/images/nametag_web.gif) 7 | 8 | Pre-requisites 9 | -------------- 10 | * Raspberry Pi 2 or 3 running Raspbian in desktop mode. 11 | * DYMO LabelWriter 450 printer connected to the Raspberry Pi via USB. 12 | * Internet access for the Raspberry Pi. 13 | 14 | The Bill of Materials provides the complete list of equipments, pricing, and links to recommended products. 15 | 16 |
**Bill of Materials (click to open)**

17 | 18 | | Item No. | Description | Quantity | Price | Link | 19 | |----------|---------------|----------|-------|------| 20 | | 1 | Raspberry Pi 3 Model B | 1 | $38.31 | [Amazon link](https://www.amazon.com/Raspberry-Pi-RASPBERRYPI3-MODB-1GB-Model-Motherboard/dp/B01CD5VC92) | 21 | | 2 | Raspberry Pi 7" Touchscreen Display | 1 | $66.99 | [Amazon link](https://www.amazon.com/Raspberry-Pi-7-Touchscreen-Display/dp/B0153R2A9I/) | 22 | | 3 | Power Adapter | 1| $9.99| [Amazon link](https://www.amazon.com/CanaKit-Raspberry-Supply-Adapter-Charger/dp/B00MARDJZ4/) | 23 | | 4 | Keyboard | 1 | $14.99| [Amazon link](https://www.amazon.com/Anker-Bluetooth-Ultra-Slim-Keyboard-Devices/dp/B005ONMDYE/) | 24 | | 5 | Micro SD Card | 1| $15.95| [Amazon link](https://www.amazon.com/Samsung-Class-Micro-Adapter-MB-MC32DA/dp/B00WR4IJBE/) | 25 | | 6 | DYMO LabelWriter 450 | 1| $66.95| [Amazon link](https://www.amazon.com/DYMO-LabelWriter-Thermal-Printer-1752264/dp/B0027JBLV4) | 26 | | 7 | DYMO 2-1/4" x 4" labels (30857) | 1 | $10.00 | [Amazon link](https://www.amazon.com/DYMO-Adhesive-LabelWriter-Printers-30857/dp/B00009WO6F) | 27 | 28 | **Total Cost:** $223.18 29 | 30 |

31 | 32 | Setting up your Raspberry Pi as a name tag kiosk 33 | ------------------------------------------------- 34 | Setting up your Raspberry Pi as a name tag kiosk involves two steps: 35 | 36 | 1. [Installing the login system](#installing_login) 37 | 2. [Adding the DYMO LabelWriter 450 printer](#adding_printer) 38 | 39 | Optional step: 40 | ------------- 41 | 3. [Uploading to Google sheets](#uploading_data) 42 | 43 | 44 | ### Installing the login system 45 | 46 | 1. Open a terminal and create a new folder called "GitHub". Run: `mkdir ~/GitHub` 47 | 48 | **NOTE:** Ensure that the folder name is "GitHub" since the folder name is referenced in the install script. 49 | 2. Clone the git repository from GitHub. Run: 50 | 51 | ``` 52 | cd ~/GitHub 53 | git clone https://github.com/codeforsanjose/CFSJ-Login-System.git 54 | cd CFSJ-Login-System/ 55 | ``` 56 | 3. Run the install script: `./install/install.sh` 57 | 58 | The installation process may take several minutes to complete. The script installs all the dependencies required for the program including the driver for the DYMO LabelWriter 450 printer. The script also configures the Raspberry Pi to start Chromium automatically on startup. 59 | 60 | ### Adding the DYMO LabelWriter 450 printer 61 | 62 | 1. Open Chromium and browse to [http://localhost:631/](http://localhost:631/). 63 | 2. Click the **Administration** tab at the top and click **Add Printer** under Printers. 64 | 3. In the **Authentication Required** dialog box, enter `pi` as the user name and `raspberry` as the password. 65 | 4. Click **Log In**. 66 | 5. In the Add Printer page, select **DYMO LabelWriter 450 (DYMO LabelWriter 450)** and click **Continue**. 67 | 6. Review the Name and Description and click **Continue**. 68 | 7. Select **DYMO LabelWriter 450 (en)** from the Model list. 69 | 8. Click **Add Printer**. 70 | 9. In the Set Default Options for DYMO_LabelWriter_450 page, set the following: 71 | * Media size: **30857 Badge Label** 72 | * Output Resolution: **300x600 DPI** 73 | * Halftoning: **Error Diffusion** 74 | * Print Density: **Normal** 75 | * Print Quality: **Barcodes and Graphics** 76 | 10. Click **Set Default Options**. You will be redirected to the Printers tab. 77 | 11. Click the **Administration** drop-down and select **Set As Server Default**. 78 | 12. Finally, close the browser and reboot the device. 79 | 80 | After rebooting, the Raspberry Pi will automatically start Chromium in kiosk mode and display the login system. 81 | 82 | 83 | ### Uploading to Google sheets 84 | 85 | 1. Open Chromium and browse to [https://developers.google.com/sheets/api/quickstart/python](https://developers.google.com/sheets/api/quickstart/python). 86 | 2. Follow the instructions under "Step 1: Turn on the Google Sheets API" to create and download client_secret.json. 87 | 3. Copy the file into the CFSJ-Login-System folder. 88 | 89 | The uploder.py copies the user name and email address of the guest from the `.csv` file into Google sheets. After a successful update, the `.csv` file is deleted. If the update fails, the user information is retained in the `.csv` file until a successful retry. 90 | 91 | -------------------------------------------------------------------------------- /uploader.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | from config import CSV_FILENAME, SPREADSHEET_ID, LOGGING_ID 3 | from pathlib import Path 4 | import httplib2 5 | import os, sys, csv, time, logging 6 | 7 | from apiclient import discovery 8 | from oauth2client import client 9 | from oauth2client import tools 10 | from oauth2client.file import Storage 11 | 12 | try: 13 | import argparse 14 | flags = argparse.ArgumentParser(parents=[tools.argparser]).parse_args() 15 | except ImportError: 16 | flags = None 17 | 18 | logger = logging.getLogger(LOGGING_ID) 19 | 20 | # If modifying these scopes, delete your previously saved credentials 21 | # at ~/.credentials/sheets.googleapis.com-python-quickstart.json 22 | SCOPES = 'https://www.googleapis.com/auth/spreadsheets' 23 | CLIENT_SECRET_FILE = 'client_secret.json' 24 | APPLICATION_NAME = 'Google Sheets API Python Quickstart' 25 | 26 | def get_script_path(): 27 | """ 28 | http://stackoverflow.com/a/4943474 29 | """ 30 | return os.path.dirname(os.path.realpath(sys.argv[0])) 31 | 32 | def read_csv(csv_filename): 33 | contents = [] 34 | with open(csv_filename) as csvfile: 35 | csvReader = csv.reader(csvfile) 36 | next(csvReader) # skip the header row 37 | for row in csvReader: 38 | if len(row) > 0: 39 | contents.append(row) 40 | return contents 41 | 42 | 43 | def get_credentials(): 44 | """Gets valid user credentials from storage. 45 | 46 | If nothing has been stored, or if the stored credentials are invalid, 47 | the OAuth2 flow is completed to obtain the new credentials. 48 | 49 | Returns: 50 | Credentials, the obtained credential. 51 | """ 52 | home_dir = os.path.expanduser('~') 53 | credential_dir = os.path.join(home_dir, '.credentials') 54 | if not os.path.exists(credential_dir): 55 | os.makedirs(credential_dir) 56 | credential_path = os.path.join(credential_dir, 57 | 'sheets.googleapis.com-python-quickstart.json') 58 | 59 | store = Storage(credential_path) 60 | credentials = store.get() 61 | if not credentials or credentials.invalid: 62 | flow = client.flow_from_clientsecrets(CLIENT_SECRET_FILE, SCOPES) 63 | flow.user_agent = APPLICATION_NAME 64 | if flags: 65 | credentials = tools.run_flow(flow, store, flags) 66 | else: # Needed only for compatibility with Python 2.6 67 | credentials = tools.run(flow, store) 68 | logger.info('Storing credentials to ' + credential_path) 69 | return credentials 70 | 71 | def write_spreadsheet(values): 72 | needs_header = False 73 | csv_file = os.path.join(os.sep, get_script_path(), CSV_FILENAME) 74 | logger.info("using CSV file at {}".format(csv_file)) 75 | if not os.path.exists(csv_file): 76 | needs_header = True 77 | 78 | with open(csv_file, 'a') as csvfile: 79 | fieldnames = ["Name", "Pronoun", "Email", "Timestamp"] 80 | writer = csv.DictWriter(csvfile, fieldnames=fieldnames) 81 | if needs_header: 82 | writer.writeheader() 83 | for row in values: 84 | writer.writerow({"Name":row[0], "Pronoun":row[1], "Email":row[2], "Timestamp":row[3]}) 85 | 86 | def update_spreadsheet(spreadsheet_id, data): 87 | credentials = get_credentials() 88 | http = credentials.authorize(httplib2.Http()) 89 | discoveryUrl = ('https://sheets.googleapis.com/$discovery/rest?' 90 | 'version=v4') 91 | 92 | logger.info("data = {}".format(str(data))) 93 | values = data 94 | logger.info("values = {}".format(str(values))) 95 | values = list(filter(None, values)) 96 | if len(values) == 0: 97 | logger.info("didn't find any useful data") 98 | return 99 | sheetname = "Sheet1" 100 | spreadsheet_range = sheetname + "!A1:C" + str(len(values)) 101 | valueInputOption = "USER_ENTERED" 102 | body = { 103 | 'values': values 104 | } 105 | try: 106 | service = discovery.build('sheets', 'v4', http=http, 107 | discoveryServiceUrl=discoveryUrl) 108 | response = service.spreadsheets().values().append(spreadsheetId = spreadsheet_id, 109 | range = spreadsheet_range, valueInputOption = valueInputOption, body=body).execute() 110 | n_updates = response.get("updates")["updatedRows"] 111 | if n_updates == len(values): 112 | logger.info("Successfully updated Google sheets. Deleting "+CSV_FILENAME) 113 | except (httplib2.ServerNotFoundError, TimeoutError) as e: 114 | logger.info("Failed to update Google sheets. Look at " +CSV_FILENAME +" for updates.") 115 | write_spreadsheet(values) 116 | 117 | def main(): 118 | """Shows basic usage of the Sheets API. 119 | 120 | Creates a Sheets API service object and prints the names and majors of 121 | students in a sample spreadsheet: 122 | https://docs.google.com/spreadsheets/d/1e6PV5ejmVUXSXiHPJR033pR1d-wKvLR8G9xWr_n4wtc/edit 123 | """ 124 | 125 | while (True): 126 | csv_file = Path(CSV_FILENAME) 127 | if csv_file.is_file(): 128 | logger.info("File found. Updating Google spreadsheet.") 129 | user_data = read_csv(CSV_FILENAME) 130 | os.remove(CSV_FILENAME) 131 | update_spreadsheet(SPREADSHEET_ID, user_data) 132 | else: 133 | logger.info("Nothing to upload.") 134 | 135 | time.sleep(30) #Time delay in seconds 136 | 137 | if __name__ == '__main__': 138 | main() 139 | 140 | -------------------------------------------------------------------------------- /static/js/nametag.js: -------------------------------------------------------------------------------- 1 | 2 | var SERVER_ENDPOINT = '/print'; 3 | 4 | 5 | var nametag = { 6 | canvas : null, 7 | margin_pct : 0.05, 8 | 9 | init : function (canvas_id, form_id, logo_elem, img_preview_id){ 10 | this.canvas = document.getElementById(canvas_id); 11 | this.canvas_height = this.canvas.height; 12 | this.canvas_width = this.canvas.width; 13 | this.margin = this.margin_pct * this.canvas.width; 14 | 15 | this.logo = document.getElementById(logo_elem); 16 | this.form = document.getElementById(form_id); 17 | this.img_preview = document.getElementById(img_preview_id); 18 | this.name_elem = this.form.elements["name"]; 19 | this.pronoun_elem = this.form.elements["pronoun"]; 20 | this.email_elem = this.form.elements["email"]; 21 | this.nametag_img_elem = this.form.elements["nametag_img"]; 22 | 23 | if (this.canvas.getContext) { 24 | var self = this; 25 | function createNametag(){ 26 | if (self.name_elem.value === '' && 27 | self.pronoun_elem.value === '' && 28 | self.email_elem.value === ''){ 29 | self.canvas.parentElement.classList.add('hidden') 30 | return; 31 | } 32 | self.canvas.parentElement.classList.remove('hidden') 33 | self.draw(); 34 | } 35 | this.name_elem.onkeyup = createNametag; 36 | this.pronoun_elem.onkeyup = createNametag; 37 | this.email_elem.onkeyup = createNametag; 38 | this.form.addEventListener('change', createNametag); 39 | } else { 40 | console.error('unsupported browser'); 41 | } 42 | }, 43 | 44 | draw : function(){ 45 | this.clear_canvas(); 46 | 47 | // top-center 48 | this.draw_text(); 49 | this.reset_context(); 50 | 51 | // lower-left corner 52 | this.draw_qr(); 53 | this.reset_context(); 54 | 55 | // lower-right 56 | this.draw_logo(); 57 | this.reset_context(); 58 | 59 | this.save_canvas(); 60 | }, 61 | 62 | reset_context : function(){ 63 | var ctx = this.canvas.getContext('2d'); 64 | ctx.fillStyle = "rgb(0,0,0)"; 65 | }, 66 | 67 | clear_canvas : function(){ 68 | var ctx = this.canvas.getContext('2d'); 69 | var c_height = this.canvas.height; 70 | var c_width = this.canvas.width; 71 | ctx.clearRect(0, 0, c_width, c_height); 72 | 73 | ctx.mozImageSmoothingEnabled = false; 74 | ctx.webkitImageSmoothingEnabled = false; 75 | ctx.msImageSmoothingEnabled = false; 76 | ctx.imageSmoothingEnabled = false; 77 | }, 78 | 79 | save_canvas : function(){ 80 | var img = this.canvas.toDataURL("image/png"); 81 | this.img_preview.src=img; 82 | this.nametag_img_elem.value=img; 83 | }, 84 | 85 | draw_text : function(){ 86 | var ctx = this.canvas.getContext('2d'); 87 | var c_height = this.canvas.height; 88 | var c_width = this.canvas.width; 89 | 90 | var text = this.name_elem.value + '(' + this.pronoun_elem.value + ')'; 91 | var fontface = 'sans-serif'; 92 | 93 | // fit text on canvas 94 | // http://stackoverflow.com/a/20552063 95 | // start with a large font size 96 | var fontsize=Math.floor(c_height/4); 97 | 98 | // lower the font size until the text fits the canvas 99 | do{ 100 | fontsize = fontsize - 1; 101 | ctx.font=fontsize+"px "+fontface; 102 | } while (ctx.measureText(text).width > (c_width - (this.margin*2))) 103 | 104 | // draw the text 105 | ctx.fillText(text,this.margin, this.margin+fontsize); 106 | }, 107 | 108 | draw_qr : function(){ 109 | var ctx = this.canvas.getContext('2d'); 110 | var c_height = this.canvas.height; 111 | var c_width = this.canvas.width; 112 | 113 | if (this.name_elem.value === '' && 114 | this.pronoun_elem.value === '' && 115 | this.email_elem.value === ''){ 116 | return; 117 | } 118 | 119 | var qr_text = this.name_elem.value + ';' + this.pronoun_elem.value + ';' + this.email_elem.value; 120 | 121 | // cell size 122 | var cs=Math.floor(c_height/70); 123 | 124 | // max bit limit for types: 125 | // 2 : 128 126 | // 3 : 208 127 | // 4 : 288 128 | // 5 : 368 129 | // 9 : 800 130 | // library max typeNumber is 40 131 | var typeNumber = 3; 132 | if (qr_text.length < 5) { 133 | typeNumber = 1; 134 | } else if (qr_text.length >= 5 && qr_text.length < 15) { 135 | typeNumber = 2; 136 | } else if (qr_text.length >= 15 && qr_text.length < 25) { 137 | typeNumber = 3; 138 | } else if (qr_text.length >= 25 && qr_text.length < 35) { 139 | typeNumber = 4; 140 | } else if (qr_text.length >= 35 && qr_text.length < 45) { 141 | typeNumber = 5; 142 | } else if (qr_text.length >= 45 && qr_text.length < 55) { 143 | typeNumber = 6; 144 | } 145 | var qr = new QRCode(typeNumber, QRErrorCorrectLevel.H); 146 | qr.addData(qr_text); 147 | qr.make(); 148 | var qr_pos = [0 + this.margin, c_height - this.margin - (qr.getModuleCount()*cs)]; 149 | for (var r = 0; r < qr.getModuleCount(); r++) { 150 | for (var c = 0; c < qr.getModuleCount(); c++) { 151 | if (qr.isDark(r, c) ) { 152 | ctx.fillStyle = "rgb(0,0,0)"; 153 | } else { 154 | ctx.fillStyle = "rgb(255,255,255)"; 155 | } 156 | ctx.fillRect (c*cs + qr_pos[0], r*cs + qr_pos[1],cs,cs); 157 | } 158 | } 159 | }, 160 | 161 | draw_logo : function(){ 162 | var ctx = this.canvas.getContext('2d'); 163 | var c_height = this.canvas.height; 164 | var c_width = this.canvas.width; 165 | 166 | // TODO: adjust image height/width based on a percentage 167 | // of the canvas height/width and image height/width 168 | var img = this.logo; 169 | // lower-right corner 170 | var image_size = this.calculateAspectRatioFit(img.width, img.height, (c_width*.5), (c_height*.5)) 171 | var x_pos = c_width - image_size.width - this.margin; 172 | var y_pos = c_height - image_size.height - this.margin; 173 | ctx.drawImage(img, x_pos, y_pos, image_size.width, image_size.height); 174 | 175 | // convert to greyscale 176 | // http://www.htmlgoodies.com/html5/javascript/display-images-in-black-and-white-using-the-html5-canvas.html 177 | var px = ctx.getImageData(x_pos, y_pos, image_size.width, image_size.height); 178 | var pixels = px.data; 179 | for (var i = 0, n = pixels.length; i < n; i += 4) { 180 | var grayscale = pixels[i] * .3 + pixels[i+1] * .59 + pixels[i+2] * .11; 181 | pixels[i ] = grayscale; // red 182 | pixels[i+1] = grayscale; // green 183 | pixels[i+2] = grayscale; // blue 184 | //pixels[i+3] is alpha 185 | } 186 | ctx.putImageData(px, x_pos, y_pos); 187 | }, 188 | 189 | calculateAspectRatioFit : function(srcWidth, srcHeight, maxWidth, maxHeight) { 190 | // http://stackoverflow.com/a/14731922 191 | var ratio = Math.min(maxWidth / srcWidth, maxHeight / srcHeight); 192 | return { width: srcWidth*ratio, height: srcHeight*ratio }; 193 | } 194 | }; 195 | 196 | 197 | (function(){ 198 | nametag.init('nametag_canvas', 'login_form', 'c4sj_logo', 'nametag_preview_surrogate'); 199 | })(); 200 | -------------------------------------------------------------------------------- /static/js/qrcode.js: -------------------------------------------------------------------------------- 1 | //--------------------------------------------------------------------- 2 | // QRCode for JavaScript 3 | // 4 | // Copyright (c) 2009 Kazuhiko Arase 5 | // 6 | // URL: http://www.d-project.com/ 7 | // 8 | // Licensed under the MIT license: 9 | // http://www.opensource.org/licenses/mit-license.php 10 | // 11 | // The word "QR Code" is registered trademark of 12 | // DENSO WAVE INCORPORATED 13 | // http://www.denso-wave.com/qrcode/faqpatent-e.html 14 | // 15 | //--------------------------------------------------------------------- 16 | 17 | //--------------------------------------------------------------------- 18 | // QR8bitByte 19 | //--------------------------------------------------------------------- 20 | 21 | function QR8bitByte(data) { 22 | this.mode = QRMode.MODE_8BIT_BYTE; 23 | this.data = data; 24 | } 25 | 26 | QR8bitByte.prototype = { 27 | 28 | getLength : function(buffer) { 29 | return this.data.length; 30 | }, 31 | 32 | write : function(buffer) { 33 | for (var i = 0; i < this.data.length; i++) { 34 | // not JIS ... 35 | buffer.put(this.data.charCodeAt(i), 8); 36 | } 37 | } 38 | }; 39 | 40 | //--------------------------------------------------------------------- 41 | // QRCode 42 | //--------------------------------------------------------------------- 43 | 44 | function QRCode(typeNumber, errorCorrectLevel) { 45 | this.typeNumber = typeNumber; 46 | this.errorCorrectLevel = errorCorrectLevel; 47 | this.modules = null; 48 | this.moduleCount = 0; 49 | this.dataCache = null; 50 | this.dataList = new Array(); 51 | } 52 | 53 | QRCode.prototype = { 54 | 55 | addData : function(data) { 56 | var newData = new QR8bitByte(data); 57 | this.dataList.push(newData); 58 | this.dataCache = null; 59 | }, 60 | 61 | isDark : function(row, col) { 62 | if (row < 0 || this.moduleCount <= row || col < 0 || this.moduleCount <= col) { 63 | throw new Error(row + "," + col); 64 | } 65 | return this.modules[row][col]; 66 | }, 67 | 68 | getModuleCount : function() { 69 | return this.moduleCount; 70 | }, 71 | 72 | make : function() { 73 | this.makeImpl(false, this.getBestMaskPattern() ); 74 | }, 75 | 76 | makeImpl : function(test, maskPattern) { 77 | 78 | this.moduleCount = this.typeNumber * 4 + 17; 79 | this.modules = new Array(this.moduleCount); 80 | 81 | for (var row = 0; row < this.moduleCount; row++) { 82 | 83 | this.modules[row] = new Array(this.moduleCount); 84 | 85 | for (var col = 0; col < this.moduleCount; col++) { 86 | this.modules[row][col] = null;//(col + row) % 3; 87 | } 88 | } 89 | 90 | this.setupPositionProbePattern(0, 0); 91 | this.setupPositionProbePattern(this.moduleCount - 7, 0); 92 | this.setupPositionProbePattern(0, this.moduleCount - 7); 93 | this.setupPositionAdjustPattern(); 94 | this.setupTimingPattern(); 95 | this.setupTypeInfo(test, maskPattern); 96 | 97 | if (this.typeNumber >= 7) { 98 | this.setupTypeNumber(test); 99 | } 100 | 101 | if (this.dataCache == null) { 102 | this.dataCache = QRCode.createData(this.typeNumber, this.errorCorrectLevel, this.dataList); 103 | } 104 | 105 | this.mapData(this.dataCache, maskPattern); 106 | }, 107 | 108 | setupPositionProbePattern : function(row, col) { 109 | 110 | for (var r = -1; r <= 7; r++) { 111 | 112 | if (row + r <= -1 || this.moduleCount <= row + r) continue; 113 | 114 | for (var c = -1; c <= 7; c++) { 115 | 116 | if (col + c <= -1 || this.moduleCount <= col + c) continue; 117 | 118 | if ( (0 <= r && r <= 6 && (c == 0 || c == 6) ) 119 | || (0 <= c && c <= 6 && (r == 0 || r == 6) ) 120 | || (2 <= r && r <= 4 && 2 <= c && c <= 4) ) { 121 | this.modules[row + r][col + c] = true; 122 | } else { 123 | this.modules[row + r][col + c] = false; 124 | } 125 | } 126 | } 127 | }, 128 | 129 | getBestMaskPattern : function() { 130 | 131 | var minLostPoint = 0; 132 | var pattern = 0; 133 | 134 | for (var i = 0; i < 8; i++) { 135 | 136 | this.makeImpl(true, i); 137 | 138 | var lostPoint = QRUtil.getLostPoint(this); 139 | 140 | if (i == 0 || minLostPoint > lostPoint) { 141 | minLostPoint = lostPoint; 142 | pattern = i; 143 | } 144 | } 145 | 146 | return pattern; 147 | }, 148 | 149 | createMovieClip : function(target_mc, instance_name, depth) { 150 | 151 | var qr_mc = target_mc.createEmptyMovieClip(instance_name, depth); 152 | var cs = 1; 153 | 154 | this.make(); 155 | 156 | for (var row = 0; row < this.modules.length; row++) { 157 | 158 | var y = row * cs; 159 | 160 | for (var col = 0; col < this.modules[row].length; col++) { 161 | 162 | var x = col * cs; 163 | var dark = this.modules[row][col]; 164 | 165 | if (dark) { 166 | qr_mc.beginFill(0, 100); 167 | qr_mc.moveTo(x, y); 168 | qr_mc.lineTo(x + cs, y); 169 | qr_mc.lineTo(x + cs, y + cs); 170 | qr_mc.lineTo(x, y + cs); 171 | qr_mc.endFill(); 172 | } 173 | } 174 | } 175 | 176 | return qr_mc; 177 | }, 178 | 179 | setupTimingPattern : function() { 180 | 181 | for (var r = 8; r < this.moduleCount - 8; r++) { 182 | if (this.modules[r][6] != null) { 183 | continue; 184 | } 185 | this.modules[r][6] = (r % 2 == 0); 186 | } 187 | 188 | for (var c = 8; c < this.moduleCount - 8; c++) { 189 | if (this.modules[6][c] != null) { 190 | continue; 191 | } 192 | this.modules[6][c] = (c % 2 == 0); 193 | } 194 | }, 195 | 196 | setupPositionAdjustPattern : function() { 197 | 198 | var pos = QRUtil.getPatternPosition(this.typeNumber); 199 | 200 | for (var i = 0; i < pos.length; i++) { 201 | 202 | for (var j = 0; j < pos.length; j++) { 203 | 204 | var row = pos[i]; 205 | var col = pos[j]; 206 | 207 | if (this.modules[row][col] != null) { 208 | continue; 209 | } 210 | 211 | for (var r = -2; r <= 2; r++) { 212 | 213 | for (var c = -2; c <= 2; c++) { 214 | 215 | if (r == -2 || r == 2 || c == -2 || c == 2 216 | || (r == 0 && c == 0) ) { 217 | this.modules[row + r][col + c] = true; 218 | } else { 219 | this.modules[row + r][col + c] = false; 220 | } 221 | } 222 | } 223 | } 224 | } 225 | }, 226 | 227 | setupTypeNumber : function(test) { 228 | 229 | var bits = QRUtil.getBCHTypeNumber(this.typeNumber); 230 | 231 | for (var i = 0; i < 18; i++) { 232 | var mod = (!test && ( (bits >> i) & 1) == 1); 233 | this.modules[Math.floor(i / 3)][i % 3 + this.moduleCount - 8 - 3] = mod; 234 | } 235 | 236 | for (var i = 0; i < 18; i++) { 237 | var mod = (!test && ( (bits >> i) & 1) == 1); 238 | this.modules[i % 3 + this.moduleCount - 8 - 3][Math.floor(i / 3)] = mod; 239 | } 240 | }, 241 | 242 | setupTypeInfo : function(test, maskPattern) { 243 | 244 | var data = (this.errorCorrectLevel << 3) | maskPattern; 245 | var bits = QRUtil.getBCHTypeInfo(data); 246 | 247 | // vertical 248 | for (var i = 0; i < 15; i++) { 249 | 250 | var mod = (!test && ( (bits >> i) & 1) == 1); 251 | 252 | if (i < 6) { 253 | this.modules[i][8] = mod; 254 | } else if (i < 8) { 255 | this.modules[i + 1][8] = mod; 256 | } else { 257 | this.modules[this.moduleCount - 15 + i][8] = mod; 258 | } 259 | } 260 | 261 | // horizontal 262 | for (var i = 0; i < 15; i++) { 263 | 264 | var mod = (!test && ( (bits >> i) & 1) == 1); 265 | 266 | if (i < 8) { 267 | this.modules[8][this.moduleCount - i - 1] = mod; 268 | } else if (i < 9) { 269 | this.modules[8][15 - i - 1 + 1] = mod; 270 | } else { 271 | this.modules[8][15 - i - 1] = mod; 272 | } 273 | } 274 | 275 | // fixed module 276 | this.modules[this.moduleCount - 8][8] = (!test); 277 | 278 | }, 279 | 280 | mapData : function(data, maskPattern) { 281 | 282 | var inc = -1; 283 | var row = this.moduleCount - 1; 284 | var bitIndex = 7; 285 | var byteIndex = 0; 286 | 287 | for (var col = this.moduleCount - 1; col > 0; col -= 2) { 288 | 289 | if (col == 6) col--; 290 | 291 | while (true) { 292 | 293 | for (var c = 0; c < 2; c++) { 294 | 295 | if (this.modules[row][col - c] == null) { 296 | 297 | var dark = false; 298 | 299 | if (byteIndex < data.length) { 300 | dark = ( ( (data[byteIndex] >>> bitIndex) & 1) == 1); 301 | } 302 | 303 | var mask = QRUtil.getMask(maskPattern, row, col - c); 304 | 305 | if (mask) { 306 | dark = !dark; 307 | } 308 | 309 | this.modules[row][col - c] = dark; 310 | bitIndex--; 311 | 312 | if (bitIndex == -1) { 313 | byteIndex++; 314 | bitIndex = 7; 315 | } 316 | } 317 | } 318 | 319 | row += inc; 320 | 321 | if (row < 0 || this.moduleCount <= row) { 322 | row -= inc; 323 | inc = -inc; 324 | break; 325 | } 326 | } 327 | } 328 | 329 | } 330 | 331 | }; 332 | 333 | QRCode.PAD0 = 0xEC; 334 | QRCode.PAD1 = 0x11; 335 | 336 | QRCode.createData = function(typeNumber, errorCorrectLevel, dataList) { 337 | 338 | var rsBlocks = QRRSBlock.getRSBlocks(typeNumber, errorCorrectLevel); 339 | 340 | var buffer = new QRBitBuffer(); 341 | 342 | for (var i = 0; i < dataList.length; i++) { 343 | var data = dataList[i]; 344 | buffer.put(data.mode, 4); 345 | buffer.put(data.getLength(), QRUtil.getLengthInBits(data.mode, typeNumber) ); 346 | data.write(buffer); 347 | } 348 | 349 | // calc num max data. 350 | var totalDataCount = 0; 351 | for (var i = 0; i < rsBlocks.length; i++) { 352 | totalDataCount += rsBlocks[i].dataCount; 353 | } 354 | 355 | if (buffer.getLengthInBits() > totalDataCount * 8) { 356 | throw new Error("code length overflow. (" 357 | + buffer.getLengthInBits() 358 | + ">" 359 | + totalDataCount * 8 360 | + ")"); 361 | } 362 | 363 | // end code 364 | if (buffer.getLengthInBits() + 4 <= totalDataCount * 8) { 365 | buffer.put(0, 4); 366 | } 367 | 368 | // padding 369 | while (buffer.getLengthInBits() % 8 != 0) { 370 | buffer.putBit(false); 371 | } 372 | 373 | // padding 374 | while (true) { 375 | 376 | if (buffer.getLengthInBits() >= totalDataCount * 8) { 377 | break; 378 | } 379 | buffer.put(QRCode.PAD0, 8); 380 | 381 | if (buffer.getLengthInBits() >= totalDataCount * 8) { 382 | break; 383 | } 384 | buffer.put(QRCode.PAD1, 8); 385 | } 386 | 387 | return QRCode.createBytes(buffer, rsBlocks); 388 | } 389 | 390 | QRCode.createBytes = function(buffer, rsBlocks) { 391 | 392 | var offset = 0; 393 | 394 | var maxDcCount = 0; 395 | var maxEcCount = 0; 396 | 397 | var dcdata = new Array(rsBlocks.length); 398 | var ecdata = new Array(rsBlocks.length); 399 | 400 | for (var r = 0; r < rsBlocks.length; r++) { 401 | 402 | var dcCount = rsBlocks[r].dataCount; 403 | var ecCount = rsBlocks[r].totalCount - dcCount; 404 | 405 | maxDcCount = Math.max(maxDcCount, dcCount); 406 | maxEcCount = Math.max(maxEcCount, ecCount); 407 | 408 | dcdata[r] = new Array(dcCount); 409 | 410 | for (var i = 0; i < dcdata[r].length; i++) { 411 | dcdata[r][i] = 0xff & buffer.buffer[i + offset]; 412 | } 413 | offset += dcCount; 414 | 415 | var rsPoly = QRUtil.getErrorCorrectPolynomial(ecCount); 416 | var rawPoly = new QRPolynomial(dcdata[r], rsPoly.getLength() - 1); 417 | 418 | var modPoly = rawPoly.mod(rsPoly); 419 | ecdata[r] = new Array(rsPoly.getLength() - 1); 420 | for (var i = 0; i < ecdata[r].length; i++) { 421 | var modIndex = i + modPoly.getLength() - ecdata[r].length; 422 | ecdata[r][i] = (modIndex >= 0)? modPoly.get(modIndex) : 0; 423 | } 424 | 425 | } 426 | 427 | var totalCodeCount = 0; 428 | for (var i = 0; i < rsBlocks.length; i++) { 429 | totalCodeCount += rsBlocks[i].totalCount; 430 | } 431 | 432 | var data = new Array(totalCodeCount); 433 | var index = 0; 434 | 435 | for (var i = 0; i < maxDcCount; i++) { 436 | for (var r = 0; r < rsBlocks.length; r++) { 437 | if (i < dcdata[r].length) { 438 | data[index++] = dcdata[r][i]; 439 | } 440 | } 441 | } 442 | 443 | for (var i = 0; i < maxEcCount; i++) { 444 | for (var r = 0; r < rsBlocks.length; r++) { 445 | if (i < ecdata[r].length) { 446 | data[index++] = ecdata[r][i]; 447 | } 448 | } 449 | } 450 | 451 | return data; 452 | 453 | } 454 | 455 | //--------------------------------------------------------------------- 456 | // QRMode 457 | //--------------------------------------------------------------------- 458 | 459 | var QRMode = { 460 | MODE_NUMBER : 1 << 0, 461 | MODE_ALPHA_NUM : 1 << 1, 462 | MODE_8BIT_BYTE : 1 << 2, 463 | MODE_KANJI : 1 << 3 464 | }; 465 | 466 | //--------------------------------------------------------------------- 467 | // QRErrorCorrectLevel 468 | //--------------------------------------------------------------------- 469 | 470 | var QRErrorCorrectLevel = { 471 | L : 1, 472 | M : 0, 473 | Q : 3, 474 | H : 2 475 | }; 476 | 477 | //--------------------------------------------------------------------- 478 | // QRMaskPattern 479 | //--------------------------------------------------------------------- 480 | 481 | var QRMaskPattern = { 482 | PATTERN000 : 0, 483 | PATTERN001 : 1, 484 | PATTERN010 : 2, 485 | PATTERN011 : 3, 486 | PATTERN100 : 4, 487 | PATTERN101 : 5, 488 | PATTERN110 : 6, 489 | PATTERN111 : 7 490 | }; 491 | 492 | //--------------------------------------------------------------------- 493 | // QRUtil 494 | //--------------------------------------------------------------------- 495 | 496 | var QRUtil = { 497 | 498 | PATTERN_POSITION_TABLE : [ 499 | [], 500 | [6, 18], 501 | [6, 22], 502 | [6, 26], 503 | [6, 30], 504 | [6, 34], 505 | [6, 22, 38], 506 | [6, 24, 42], 507 | [6, 26, 46], 508 | [6, 28, 50], 509 | [6, 30, 54], 510 | [6, 32, 58], 511 | [6, 34, 62], 512 | [6, 26, 46, 66], 513 | [6, 26, 48, 70], 514 | [6, 26, 50, 74], 515 | [6, 30, 54, 78], 516 | [6, 30, 56, 82], 517 | [6, 30, 58, 86], 518 | [6, 34, 62, 90], 519 | [6, 28, 50, 72, 94], 520 | [6, 26, 50, 74, 98], 521 | [6, 30, 54, 78, 102], 522 | [6, 28, 54, 80, 106], 523 | [6, 32, 58, 84, 110], 524 | [6, 30, 58, 86, 114], 525 | [6, 34, 62, 90, 118], 526 | [6, 26, 50, 74, 98, 122], 527 | [6, 30, 54, 78, 102, 126], 528 | [6, 26, 52, 78, 104, 130], 529 | [6, 30, 56, 82, 108, 134], 530 | [6, 34, 60, 86, 112, 138], 531 | [6, 30, 58, 86, 114, 142], 532 | [6, 34, 62, 90, 118, 146], 533 | [6, 30, 54, 78, 102, 126, 150], 534 | [6, 24, 50, 76, 102, 128, 154], 535 | [6, 28, 54, 80, 106, 132, 158], 536 | [6, 32, 58, 84, 110, 136, 162], 537 | [6, 26, 54, 82, 110, 138, 166], 538 | [6, 30, 58, 86, 114, 142, 170] 539 | ], 540 | 541 | G15 : (1 << 10) | (1 << 8) | (1 << 5) | (1 << 4) | (1 << 2) | (1 << 1) | (1 << 0), 542 | G18 : (1 << 12) | (1 << 11) | (1 << 10) | (1 << 9) | (1 << 8) | (1 << 5) | (1 << 2) | (1 << 0), 543 | G15_MASK : (1 << 14) | (1 << 12) | (1 << 10) | (1 << 4) | (1 << 1), 544 | 545 | getBCHTypeInfo : function(data) { 546 | var d = data << 10; 547 | while (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G15) >= 0) { 548 | d ^= (QRUtil.G15 << (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G15) ) ); 549 | } 550 | return ( (data << 10) | d) ^ QRUtil.G15_MASK; 551 | }, 552 | 553 | getBCHTypeNumber : function(data) { 554 | var d = data << 12; 555 | while (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G18) >= 0) { 556 | d ^= (QRUtil.G18 << (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G18) ) ); 557 | } 558 | return (data << 12) | d; 559 | }, 560 | 561 | getBCHDigit : function(data) { 562 | 563 | var digit = 0; 564 | 565 | while (data != 0) { 566 | digit++; 567 | data >>>= 1; 568 | } 569 | 570 | return digit; 571 | }, 572 | 573 | getPatternPosition : function(typeNumber) { 574 | return QRUtil.PATTERN_POSITION_TABLE[typeNumber - 1]; 575 | }, 576 | 577 | getMask : function(maskPattern, i, j) { 578 | 579 | switch (maskPattern) { 580 | 581 | case QRMaskPattern.PATTERN000 : return (i + j) % 2 == 0; 582 | case QRMaskPattern.PATTERN001 : return i % 2 == 0; 583 | case QRMaskPattern.PATTERN010 : return j % 3 == 0; 584 | case QRMaskPattern.PATTERN011 : return (i + j) % 3 == 0; 585 | case QRMaskPattern.PATTERN100 : return (Math.floor(i / 2) + Math.floor(j / 3) ) % 2 == 0; 586 | case QRMaskPattern.PATTERN101 : return (i * j) % 2 + (i * j) % 3 == 0; 587 | case QRMaskPattern.PATTERN110 : return ( (i * j) % 2 + (i * j) % 3) % 2 == 0; 588 | case QRMaskPattern.PATTERN111 : return ( (i * j) % 3 + (i + j) % 2) % 2 == 0; 589 | 590 | default : 591 | throw new Error("bad maskPattern:" + maskPattern); 592 | } 593 | }, 594 | 595 | getErrorCorrectPolynomial : function(errorCorrectLength) { 596 | 597 | var a = new QRPolynomial([1], 0); 598 | 599 | for (var i = 0; i < errorCorrectLength; i++) { 600 | a = a.multiply(new QRPolynomial([1, QRMath.gexp(i)], 0) ); 601 | } 602 | 603 | return a; 604 | }, 605 | 606 | getLengthInBits : function(mode, type) { 607 | 608 | if (1 <= type && type < 10) { 609 | 610 | // 1 - 9 611 | 612 | switch(mode) { 613 | case QRMode.MODE_NUMBER : return 10; 614 | case QRMode.MODE_ALPHA_NUM : return 9; 615 | case QRMode.MODE_8BIT_BYTE : return 8; 616 | case QRMode.MODE_KANJI : return 8; 617 | default : 618 | throw new Error("mode:" + mode); 619 | } 620 | 621 | } else if (type < 27) { 622 | 623 | // 10 - 26 624 | 625 | switch(mode) { 626 | case QRMode.MODE_NUMBER : return 12; 627 | case QRMode.MODE_ALPHA_NUM : return 11; 628 | case QRMode.MODE_8BIT_BYTE : return 16; 629 | case QRMode.MODE_KANJI : return 10; 630 | default : 631 | throw new Error("mode:" + mode); 632 | } 633 | 634 | } else if (type < 41) { 635 | 636 | // 27 - 40 637 | 638 | switch(mode) { 639 | case QRMode.MODE_NUMBER : return 14; 640 | case QRMode.MODE_ALPHA_NUM : return 13; 641 | case QRMode.MODE_8BIT_BYTE : return 16; 642 | case QRMode.MODE_KANJI : return 12; 643 | default : 644 | throw new Error("mode:" + mode); 645 | } 646 | 647 | } else { 648 | throw new Error("type:" + type); 649 | } 650 | }, 651 | 652 | getLostPoint : function(qrCode) { 653 | 654 | var moduleCount = qrCode.getModuleCount(); 655 | 656 | var lostPoint = 0; 657 | 658 | // LEVEL1 659 | 660 | for (var row = 0; row < moduleCount; row++) { 661 | 662 | for (var col = 0; col < moduleCount; col++) { 663 | 664 | var sameCount = 0; 665 | var dark = qrCode.isDark(row, col); 666 | 667 | for (var r = -1; r <= 1; r++) { 668 | 669 | if (row + r < 0 || moduleCount <= row + r) { 670 | continue; 671 | } 672 | 673 | for (var c = -1; c <= 1; c++) { 674 | 675 | if (col + c < 0 || moduleCount <= col + c) { 676 | continue; 677 | } 678 | 679 | if (r == 0 && c == 0) { 680 | continue; 681 | } 682 | 683 | if (dark == qrCode.isDark(row + r, col + c) ) { 684 | sameCount++; 685 | } 686 | } 687 | } 688 | 689 | if (sameCount > 5) { 690 | lostPoint += (3 + sameCount - 5); 691 | } 692 | } 693 | } 694 | 695 | // LEVEL2 696 | 697 | for (var row = 0; row < moduleCount - 1; row++) { 698 | for (var col = 0; col < moduleCount - 1; col++) { 699 | var count = 0; 700 | if (qrCode.isDark(row, col ) ) count++; 701 | if (qrCode.isDark(row + 1, col ) ) count++; 702 | if (qrCode.isDark(row, col + 1) ) count++; 703 | if (qrCode.isDark(row + 1, col + 1) ) count++; 704 | if (count == 0 || count == 4) { 705 | lostPoint += 3; 706 | } 707 | } 708 | } 709 | 710 | // LEVEL3 711 | 712 | for (var row = 0; row < moduleCount; row++) { 713 | for (var col = 0; col < moduleCount - 6; col++) { 714 | if (qrCode.isDark(row, col) 715 | && !qrCode.isDark(row, col + 1) 716 | && qrCode.isDark(row, col + 2) 717 | && qrCode.isDark(row, col + 3) 718 | && qrCode.isDark(row, col + 4) 719 | && !qrCode.isDark(row, col + 5) 720 | && qrCode.isDark(row, col + 6) ) { 721 | lostPoint += 40; 722 | } 723 | } 724 | } 725 | 726 | for (var col = 0; col < moduleCount; col++) { 727 | for (var row = 0; row < moduleCount - 6; row++) { 728 | if (qrCode.isDark(row, col) 729 | && !qrCode.isDark(row + 1, col) 730 | && qrCode.isDark(row + 2, col) 731 | && qrCode.isDark(row + 3, col) 732 | && qrCode.isDark(row + 4, col) 733 | && !qrCode.isDark(row + 5, col) 734 | && qrCode.isDark(row + 6, col) ) { 735 | lostPoint += 40; 736 | } 737 | } 738 | } 739 | 740 | // LEVEL4 741 | 742 | var darkCount = 0; 743 | 744 | for (var col = 0; col < moduleCount; col++) { 745 | for (var row = 0; row < moduleCount; row++) { 746 | if (qrCode.isDark(row, col) ) { 747 | darkCount++; 748 | } 749 | } 750 | } 751 | 752 | var ratio = Math.abs(100 * darkCount / moduleCount / moduleCount - 50) / 5; 753 | lostPoint += ratio * 10; 754 | 755 | return lostPoint; 756 | } 757 | 758 | }; 759 | 760 | 761 | //--------------------------------------------------------------------- 762 | // QRMath 763 | //--------------------------------------------------------------------- 764 | 765 | var QRMath = { 766 | 767 | glog : function(n) { 768 | 769 | if (n < 1) { 770 | throw new Error("glog(" + n + ")"); 771 | } 772 | 773 | return QRMath.LOG_TABLE[n]; 774 | }, 775 | 776 | gexp : function(n) { 777 | 778 | while (n < 0) { 779 | n += 255; 780 | } 781 | 782 | while (n >= 256) { 783 | n -= 255; 784 | } 785 | 786 | return QRMath.EXP_TABLE[n]; 787 | }, 788 | 789 | EXP_TABLE : new Array(256), 790 | 791 | LOG_TABLE : new Array(256) 792 | 793 | }; 794 | 795 | for (var i = 0; i < 8; i++) { 796 | QRMath.EXP_TABLE[i] = 1 << i; 797 | } 798 | for (var i = 8; i < 256; i++) { 799 | QRMath.EXP_TABLE[i] = QRMath.EXP_TABLE[i - 4] 800 | ^ QRMath.EXP_TABLE[i - 5] 801 | ^ QRMath.EXP_TABLE[i - 6] 802 | ^ QRMath.EXP_TABLE[i - 8]; 803 | } 804 | for (var i = 0; i < 255; i++) { 805 | QRMath.LOG_TABLE[QRMath.EXP_TABLE[i] ] = i; 806 | } 807 | 808 | //--------------------------------------------------------------------- 809 | // QRPolynomial 810 | //--------------------------------------------------------------------- 811 | 812 | function QRPolynomial(num, shift) { 813 | 814 | if (num.length == undefined) { 815 | throw new Error(num.length + "/" + shift); 816 | } 817 | 818 | var offset = 0; 819 | 820 | while (offset < num.length && num[offset] == 0) { 821 | offset++; 822 | } 823 | 824 | this.num = new Array(num.length - offset + shift); 825 | for (var i = 0; i < num.length - offset; i++) { 826 | this.num[i] = num[i + offset]; 827 | } 828 | } 829 | 830 | QRPolynomial.prototype = { 831 | 832 | get : function(index) { 833 | return this.num[index]; 834 | }, 835 | 836 | getLength : function() { 837 | return this.num.length; 838 | }, 839 | 840 | multiply : function(e) { 841 | 842 | var num = new Array(this.getLength() + e.getLength() - 1); 843 | 844 | for (var i = 0; i < this.getLength(); i++) { 845 | for (var j = 0; j < e.getLength(); j++) { 846 | num[i + j] ^= QRMath.gexp(QRMath.glog(this.get(i) ) + QRMath.glog(e.get(j) ) ); 847 | } 848 | } 849 | 850 | return new QRPolynomial(num, 0); 851 | }, 852 | 853 | mod : function(e) { 854 | 855 | if (this.getLength() - e.getLength() < 0) { 856 | return this; 857 | } 858 | 859 | var ratio = QRMath.glog(this.get(0) ) - QRMath.glog(e.get(0) ); 860 | 861 | var num = new Array(this.getLength() ); 862 | 863 | for (var i = 0; i < this.getLength(); i++) { 864 | num[i] = this.get(i); 865 | } 866 | 867 | for (var i = 0; i < e.getLength(); i++) { 868 | num[i] ^= QRMath.gexp(QRMath.glog(e.get(i) ) + ratio); 869 | } 870 | 871 | // recursive call 872 | return new QRPolynomial(num, 0).mod(e); 873 | } 874 | }; 875 | 876 | //--------------------------------------------------------------------- 877 | // QRRSBlock 878 | //--------------------------------------------------------------------- 879 | 880 | function QRRSBlock(totalCount, dataCount) { 881 | this.totalCount = totalCount; 882 | this.dataCount = dataCount; 883 | } 884 | 885 | QRRSBlock.RS_BLOCK_TABLE = [ 886 | 887 | // L 888 | // M 889 | // Q 890 | // H 891 | 892 | // 1 893 | [1, 26, 19], 894 | [1, 26, 16], 895 | [1, 26, 13], 896 | [1, 26, 9], 897 | 898 | // 2 899 | [1, 44, 34], 900 | [1, 44, 28], 901 | [1, 44, 22], 902 | [1, 44, 16], 903 | 904 | // 3 905 | [1, 70, 55], 906 | [1, 70, 44], 907 | [2, 35, 17], 908 | [2, 35, 13], 909 | 910 | // 4 911 | [1, 100, 80], 912 | [2, 50, 32], 913 | [2, 50, 24], 914 | [4, 25, 9], 915 | 916 | // 5 917 | [1, 134, 108], 918 | [2, 67, 43], 919 | [2, 33, 15, 2, 34, 16], 920 | [2, 33, 11, 2, 34, 12], 921 | 922 | // 6 923 | [2, 86, 68], 924 | [4, 43, 27], 925 | [4, 43, 19], 926 | [4, 43, 15], 927 | 928 | // 7 929 | [2, 98, 78], 930 | [4, 49, 31], 931 | [2, 32, 14, 4, 33, 15], 932 | [4, 39, 13, 1, 40, 14], 933 | 934 | // 8 935 | [2, 121, 97], 936 | [2, 60, 38, 2, 61, 39], 937 | [4, 40, 18, 2, 41, 19], 938 | [4, 40, 14, 2, 41, 15], 939 | 940 | // 9 941 | [2, 146, 116], 942 | [3, 58, 36, 2, 59, 37], 943 | [4, 36, 16, 4, 37, 17], 944 | [4, 36, 12, 4, 37, 13], 945 | 946 | // 10 947 | [2, 86, 68, 2, 87, 69], 948 | [4, 69, 43, 1, 70, 44], 949 | [6, 43, 19, 2, 44, 20], 950 | [6, 43, 15, 2, 44, 16] 951 | 952 | ]; 953 | 954 | QRRSBlock.getRSBlocks = function(typeNumber, errorCorrectLevel) { 955 | 956 | var rsBlock = QRRSBlock.getRsBlockTable(typeNumber, errorCorrectLevel); 957 | 958 | if (rsBlock == undefined) { 959 | throw new Error("bad rs block @ typeNumber:" + typeNumber + "/errorCorrectLevel:" + errorCorrectLevel); 960 | } 961 | 962 | var length = rsBlock.length / 3; 963 | 964 | var list = new Array(); 965 | 966 | for (var i = 0; i < length; i++) { 967 | 968 | var count = rsBlock[i * 3 + 0]; 969 | var totalCount = rsBlock[i * 3 + 1]; 970 | var dataCount = rsBlock[i * 3 + 2]; 971 | 972 | for (var j = 0; j < count; j++) { 973 | list.push(new QRRSBlock(totalCount, dataCount) ); 974 | } 975 | } 976 | 977 | return list; 978 | } 979 | 980 | QRRSBlock.getRsBlockTable = function(typeNumber, errorCorrectLevel) { 981 | 982 | switch(errorCorrectLevel) { 983 | case QRErrorCorrectLevel.L : 984 | return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 0]; 985 | case QRErrorCorrectLevel.M : 986 | return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 1]; 987 | case QRErrorCorrectLevel.Q : 988 | return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 2]; 989 | case QRErrorCorrectLevel.H : 990 | return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 3]; 991 | default : 992 | return undefined; 993 | } 994 | } 995 | 996 | //--------------------------------------------------------------------- 997 | // QRBitBuffer 998 | //--------------------------------------------------------------------- 999 | 1000 | function QRBitBuffer() { 1001 | this.buffer = new Array(); 1002 | this.length = 0; 1003 | } 1004 | 1005 | QRBitBuffer.prototype = { 1006 | 1007 | get : function(index) { 1008 | var bufIndex = Math.floor(index / 8); 1009 | return ( (this.buffer[bufIndex] >>> (7 - index % 8) ) & 1) == 1; 1010 | }, 1011 | 1012 | put : function(num, length) { 1013 | for (var i = 0; i < length; i++) { 1014 | this.putBit( ( (num >>> (length - i - 1) ) & 1) == 1); 1015 | } 1016 | }, 1017 | 1018 | getLengthInBits : function() { 1019 | return this.length; 1020 | }, 1021 | 1022 | putBit : function(bit) { 1023 | 1024 | var bufIndex = Math.floor(this.length / 8); 1025 | if (this.buffer.length <= bufIndex) { 1026 | this.buffer.push(0); 1027 | } 1028 | 1029 | if (bit) { 1030 | this.buffer[bufIndex] |= (0x80 >>> (this.length % 8) ); 1031 | } 1032 | 1033 | this.length++; 1034 | } 1035 | }; 1036 | --------------------------------------------------------------------------------