├── 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 |
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 |
18 |
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 | 
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 |
--------------------------------------------------------------------------------