updating ui

This commit is contained in:
2025-05-08 20:49:15 -04:00
parent 1e14703eec
commit f5f6e52c4b
15 changed files with 1332 additions and 53 deletions

6
.gitignore vendored
View File

@@ -1,3 +1,7 @@
venv/
desktop.ini
static/
static/
__pycache__/
*.pyc
*.pyo
*.pyd

View File

@@ -25,12 +25,21 @@
* **Status Codes:** Return appropriate HTTP status codes (e.g., 200 for success, 404 for not found, 500 for server error).
* **Error Responses:** Provide consistent and descriptive error messages.
* **Data Validation:** Validate all incoming data to prevent unexpected behavior and security issues.
* **Unified API and UI:** For demo purposes, both the API endpoints and admin UI should be served by the same Flask application.
## File Organization
* **Static Files:** Store images in the `static/images/` directory.
* **Product Data:** Store product metadata in `products.json`.
* **Configuration Files:** Use a dedicated `config/` directory for environment-specific settings.
* **Templates:** Store HTML templates in the `templates/` directory, with admin-specific templates in `templates/admin/`.
## Documentation Guidelines
* **Keep Documentation Updated:** Update TASK.md as features are implemented to track progress.
* **Readme First:** README.md should always reflect the current state of the project and provide clear setup instructions.
* **Code Comments:** Keep code comments in sync with implementation changes.
* **API Documentation:** Document all API endpoints, including request/response formats.
## Security Guidelines

View File

@@ -1,22 +1,39 @@
# Flask AR API Demo
This project is a simple Flask API for demonstrating AR content retrieval for barcode scanning applications. It simulates the Knox Capture API for AR overlays and includes endpoints for managing product attributes, including text and image URLs.
This project is a simple Flask API for demonstrating AR content retrieval for barcode scanning applications. It simulates the Knox Capture API for AR overlays and includes endpoints for managing product attributes, including text and image URLs. The application includes a full admin interface for product management and barcode generation.
## Features
### API Endpoints
* **Login Endpoint (`/login`)**
* Simulates a simple login with a 200 response (no authentication required for demo purposes).
* **Content Fields Endpoint (`/arcontentfields`)**
* Returns a list of available attributes (e.g., item ID, price, image URI).
* \*\*AR Info Endpoint (`/arinfo`)
* **AR Info Endpoint (`/arinfo?barcode=<id>`)**
* Returns product details for a given barcode, including image URLs.
* \*\*Static Image Server (`/images/<filename>`)
* **Static Image Server (`/images/<filename>`)**
* Serves image files from the `static/images/` directory.
* **Barcode Image Server (`/barcodes/<filename>`)**
* Serves generated barcode images from the `static/barcodes/` directory.
### Admin Interface
* **Product Management**
* Add, edit, and delete products
* Upload product images
* View product details
* **Barcode Generation**
* Generate QR codes linking to product AR info
* Generate EAN-13 barcodes for retail use
* Generate Code 128 barcodes for general use
* Download or print all barcode formats
## Setup
1. **Clone the Repository:**
@@ -26,38 +43,71 @@ git clone <repository_url>
cd <repository_folder>
```
2. **Create the Static Directory:**
2. **Install Dependencies:**
```bash
mkdir -p static/images
pip install -r requirements.txt
```
3. **Add Sample Images:**
* Place sample product images in the `static/images/` directory.
* Ensure the filenames match the item IDs used in the app (e.g., `123456.png`, `789012.png`).
4. **Install Dependencies:**
```bash
pip install flask
```
5. **Run the Server:**
3. **Run the Server:**
```bash
python app.py
```
6. **Test the Endpoints:**
The application will automatically create the necessary directories and initialize the products.json file if it doesn't exist.
4. **Access the Admin Interface:**
Open your browser and navigate to:
```
http://localhost:5555/admin
```
5. **Test the Endpoints:**
* Login: `GET http://localhost:5555/login`
* Content Fields: `GET http://localhost:5555/arcontentfields`
* AR Info (Example): `GET http://localhost:5555/arinfo?barcode=123456`
* Image File (Example): `GET http://localhost:5555/images/123456.png`
## Future Enhancements
## Using Barcode Generation
* Add a basic admin UI for adding and managing products.
* Store product data in a JSON file for persistence.
* Add image upload capabilities through the admin UI.
1. Navigate to the admin interface at `/admin`
2. Find the product you want to generate barcodes for
3. Click the "Barcodes" button in the Actions column
4. View, download, or print the generated barcodes
The following barcode types are available:
* **QR Code**: Contains a link to the product's AR info endpoint
* **EAN-13**: Standard barcode format used in retail
* **Code 128**: High-density alphanumeric barcode format
## Project Structure
```
/
├── app.py # Main Flask application
├── admin.py # Admin blueprint with routes
├── barcode_generator.py # Barcode generation utilities
├── requirements.txt # Python dependencies
├── static/
│ ├── images/ # Product images
│ ├── barcodes/ # Generated barcode images
│ └── products.json # Product data storage
└── templates/
├── index.html # Home page
├── layout.html # Base template
└── admin/ # Admin interface templates
├── index.html # Product list
├── add_product.html # Add product form
├── edit_product.html # Edit product form
└── generate_barcode.html # Barcode generation page
```
## Implementation Notes
* The application is designed as a single server hosting both the API endpoints and admin interface.
* Product data is stored in a JSON file (products.json) for simplicity and easy demonstration.
* All features are implemented without authentication for demo purposes.
* The application uses Bootstrap 5 for responsive UI design.

39
TASK.md
View File

@@ -1,26 +1,41 @@
# Task List - Flask AR API Demo
## Phase 1: Basic Admin UI
## Phase 1: Basic Admin UI
* [ ] Create a simple admin UI (no authentication required for demo purposes).
* [ ] Allow adding, updating, and deleting products.
* [ ] Store all product data in a `products.json` file for persistence.
* [ ] Store images in the `static/images/` directory.
* [ ] Implement basic form validation for product fields (ID, price, image).
* [ ] Add a file upload feature for product images.
* [x] Create a simple admin UI (no authentication required for demo purposes).
* [x] Allow adding, updating, and deleting products.
* [x] Store all product data in a `products.json` file for persistence.
* [x] Store images in the `static/images/` directory.
* [x] Implement basic form validation for product fields (ID, price, image).
* [x] Add a file upload feature for product images.
## Phase 2: API Improvements
* [ ] Update the `/arinfo` endpoint to read from `products.json` instead of the in-memory dictionary.
* [x] Update the `/arinfo` endpoint to read from `products.json` instead of the in-memory dictionary.
* [ ] Add support for updating product attributes via the API.
* [ ] Implement error handling and input validation.
* [x] Implement error handling and input validation.
* [ ] Optimize image loading for better performance.
## Phase 3: UI Enhancements
* [ ] Add a responsive design for mobile and tablet support.
* [ ] Include image previews in the admin UI.
* [x] Add a responsive design for mobile and tablet support.
* [x] Include image previews in the admin UI.
* [ ] Add sorting and search capabilities for the product list.
* [x] Add barcode and QR code generation for products.
## Implementation Notes
* The APIs and admin UI have been implemented as a single server for demonstration purposes.
* The API endpoints and admin interface are now served by the same Flask application.
* Bootstrap 5 has been used for responsive UI design.
* Basic validation has been implemented for product forms.
* Flash messages provide user feedback for actions.
* Barcode generation features support three formats:
* QR Code: Links directly to the product's AR information endpoint
* EAN-13: Standard barcode format for retail products
* Code 128: High-density alphanumeric barcode
* Generated barcodes can be previewed, downloaded individually, or printed all at once.
* Barcodes are dynamically generated and stored in the `static/barcodes/` directory.
## Future Ideas
@@ -31,3 +46,5 @@
* [ ] Implement role-based access control (RBAC).
* [ ] Add analytics for product views and updates.
* [ ] Integrate with a database for larger product catalogs.
* [ ] Add bulk import/export functionality for products.
* [ ] Implement a mobile-friendly scanner interface for testing the AR functionality.

284
admin.py Normal file
View File

@@ -0,0 +1,284 @@
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify, send_file
import os
import json
import uuid
import io
from werkzeug.utils import secure_filename
# Import barcode generator class, but handle the case if it fails
try:
from barcode_generator import BarcodeGenerator
BARCODE_GENERATOR_AVAILABLE = True
except ImportError:
BARCODE_GENERATOR_AVAILABLE = False
# Create a stub class for graceful degradation
class BarcodeGenerator:
@staticmethod
def check_dependencies():
return {'qrcode': False, 'barcode': False, 'pillow': False}
admin_bp = Blueprint('admin', __name__, url_prefix='/admin')
PRODUCTS_FILE = 'static/products.json'
UPLOAD_FOLDER = 'static/images'
BARCODE_FOLDER = 'static/barcodes'
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
def load_products():
try:
with open(PRODUCTS_FILE, 'r') as f:
return json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
return {}
def save_products(products):
with open(PRODUCTS_FILE, 'w') as f:
json.dump(products, f, indent=2)
@admin_bp.route('/')
def index():
products = load_products()
return render_template('admin/index.html', products=products)
@admin_bp.route('/add', methods=['GET', 'POST'])
def add_product():
if request.method == 'POST':
# Get form data
product_id = request.form.get('product_id')
name = request.form.get('name')
price = request.form.get('price')
# Basic validation
if not product_id or not name or not price:
flash('Product ID, Name, and Price are required fields.')
return render_template('admin/add_product.html')
# Check if product ID already exists
products = load_products()
if product_id in products:
flash('Product ID already exists.')
return render_template('admin/add_product.html')
# Handle image upload
image_filename = f"{product_id}.png" # Default name
image_path = f"/images/{image_filename}"
if 'image' in request.files:
file = request.files['image']
if file and file.filename and allowed_file(file.filename):
extension = file.filename.rsplit('.', 1)[1].lower()
image_filename = f"{product_id}.{extension}"
image_path = f"/images/{image_filename}"
file.save(os.path.join(UPLOAD_FOLDER, image_filename))
# Create product data structure
product_data = [
{"fieldName": "_id", "label": "Item ID", "value": product_id, "editable": "false", "fieldType": "TEXT"},
{"fieldName": "_name", "label": "Product Name", "value": name, "editable": "true", "fieldType": "TEXT"},
{"fieldName": "_price", "label": "Sale Price", "value": f"${price}", "editable": "true", "fieldType": "TEXT"},
{"fieldName": "_image", "label": "Image", "value": image_path, "editable": "false", "fieldType": "IMAGE_URI"}
]
# Save to products.json
products[product_id] = product_data
save_products(products)
flash('Product added successfully!')
return redirect(url_for('admin.index'))
return render_template('admin/add_product.html')
@admin_bp.route('/edit/<product_id>', methods=['GET', 'POST'])
def edit_product(product_id):
products = load_products()
if product_id not in products:
flash('Product not found.')
return redirect(url_for('admin.index'))
if request.method == 'POST':
# Get form data
name = request.form.get('name')
price = request.form.get('price')
# Basic validation
if not name or not price:
flash('Name and Price are required fields.')
return render_template('admin/edit_product.html', product_id=product_id, product=products[product_id])
# Update name and price
for field in products[product_id]:
if field["fieldName"] == "_name":
field["value"] = name
elif field["fieldName"] == "_price":
field["value"] = f"${price}"
# Handle image upload
if 'image' in request.files and request.files['image'].filename:
file = request.files['image']
if allowed_file(file.filename):
# Find current image path
current_image = None
for field in products[product_id]:
if field["fieldName"] == "_image":
current_image = field["value"]
if current_image:
# Get current filename
current_filename = os.path.basename(current_image)
# Delete old file if it exists and is different
old_path = os.path.join(UPLOAD_FOLDER, current_filename)
if os.path.exists(old_path):
os.remove(old_path)
# Save new image
extension = file.filename.rsplit('.', 1)[1].lower()
image_filename = f"{product_id}.{extension}"
image_path = f"/images/{image_filename}"
file.save(os.path.join(UPLOAD_FOLDER, image_filename))
# Update image path
for field in products[product_id]:
if field["fieldName"] == "_image":
field["value"] = image_path
# Save to products.json
save_products(products)
flash('Product updated successfully!')
return redirect(url_for('admin.index'))
return render_template('admin/edit_product.html', product_id=product_id, product=products[product_id])
@admin_bp.route('/delete/<product_id>', methods=['POST'])
def delete_product(product_id):
products = load_products()
if product_id not in products:
flash('Product not found.')
return redirect(url_for('admin.index'))
# Find image path
image_path = None
for field in products[product_id]:
if field["fieldName"] == "_image":
image_path = field["value"]
# Delete image file
if image_path:
image_filename = os.path.basename(image_path)
image_file_path = os.path.join(UPLOAD_FOLDER, image_filename)
if os.path.exists(image_file_path):
os.remove(image_file_path)
# Delete product from dict
del products[product_id]
# Save updated products
save_products(products)
flash('Product deleted successfully!')
return redirect(url_for('admin.index'))
@admin_bp.route('/view/<product_id>')
def view_product(product_id):
products = load_products()
if product_id not in products:
flash('Product not found.')
return redirect(url_for('admin.index'))
return render_template('admin/view_product.html', product_id=product_id, product=products[product_id])
@admin_bp.route('/generate_barcode/<product_id>/<code_type>')
def generate_barcode(product_id, code_type):
"""Generate and serve a barcode or QR code for a product"""
# Make sure the barcodes directory exists
os.makedirs(BARCODE_FOLDER, exist_ok=True)
products = load_products()
if product_id not in products:
return jsonify({"error": "Product not found"}), 404
# Check if barcode generator is available
if not BARCODE_GENERATOR_AVAILABLE:
# Create a simple error image
from PIL import Image, ImageDraw, ImageFont
img = Image.new('RGB', (300, 150), color=(255, 255, 255))
d = ImageDraw.Draw(img)
d.rectangle([0, 0, 299, 149], outline=(0, 0, 0), width=2)
d.text((25, 65), "Barcode generation unavailable", fill=(0, 0, 0))
# Save and serve the error image
image_path = os.path.join(BARCODE_FOLDER, f"{product_id}_{code_type}_error.png")
img.save(image_path)
return send_file(image_path, mimetype='image/png')
generator = BarcodeGenerator()
# Content for the code - use the product ID
data = product_id
# Base filename for the saved code
base_filename = f"{product_id}_{code_type}"
image_path = os.path.join(BARCODE_FOLDER, f"{base_filename}.png")
# Generate the requested code type
if code_type == 'qrcode':
# Generate QR code with product URL
product_url = request.host_url.rstrip('/') + f"/arinfo?barcode={product_id}"
img = generator.generate_qr_code(product_url)
generator.save_image(img, image_path)
elif code_type == 'ean13':
# For EAN-13, make sure the product ID is numeric
numeric_id = ''.join(filter(str.isdigit, product_id))
img = generator.generate_ean13_barcode(numeric_id)
generator.save_image(img, image_path)
elif code_type == 'code128':
img = generator.generate_code128_barcode(data)
generator.save_image(img, image_path)
else:
return jsonify({"error": "Invalid code type"}), 400
# Return the image directly
return send_file(image_path, mimetype='image/png')
@admin_bp.route('/generate_barcode_page/<product_id>')
def generate_barcode_page(product_id):
"""Show a page with different barcode options for a product"""
products = load_products()
if product_id not in products:
flash('Product not found.')
return redirect(url_for('admin.index'))
# Extract product data
product_data = {}
for field in products[product_id]:
field_name = field["fieldName"][1:] if field["fieldName"].startswith('_') else field["fieldName"]
product_data[field_name] = field["value"]
# Add product ID
product_data['id'] = product_id
# Check if barcode generation is available
dependency_status = {}
if BARCODE_GENERATOR_AVAILABLE:
dependency_status = BarcodeGenerator.check_dependencies()
else:
dependency_status = {
'qrcode': False,
'barcode': False,
'pillow': False
}
return render_template('admin/generate_barcode.html',
product_id=product_id,
product=product_data,
dependencies=dependency_status)

68
app.py
View File

@@ -1,22 +1,26 @@
from flask import Flask, jsonify, request, send_file
from flask import Flask, jsonify, request, send_file, render_template, flash
import os
import json
from admin import admin_bp
app = Flask(__name__)
app.config['STATIC_FOLDER'] = './static'
app.config['SECRET_KEY'] = 'dev-key-for-demo-only'
# Sample in-memory data for item attributes
ITEM_ATTRIBUTES = {
"123456": [
{"fieldName": "_id", "label": "Item ID", "value": "123456", "editable": "false", "fieldType": "TEXT"},
{"fieldName": "_price", "label": "Sale Price", "value": "$49.99", "editable": "true", "fieldType": "TEXT"},
{"fieldName": "_image", "label": "Image", "value": "/images/123456.png", "editable": "false", "fieldType": "IMAGE_URI"}
],
"789012": [
{"fieldName": "_id", "label": "Item ID", "value": "789012", "editable": "false", "fieldType": "TEXT"},
{"fieldName": "_price", "label": "Sale Price", "value": "$59.99", "editable": "true", "fieldType": "TEXT"},
{"fieldName": "_image", "label": "Image", "value": "/images/789012.png", "editable": "false", "fieldType": "IMAGE_URI"}
]
}
# Register blueprints
app.register_blueprint(admin_bp)
# Load product data from products.json
def load_products():
try:
with open('static/products.json', 'r') as f:
return json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
return {}
@app.route('/')
def index():
return render_template('index.html')
@app.route('/login', methods=['GET'])
def login():
@@ -36,16 +40,46 @@ def get_ar_content_fields():
@app.route('/arinfo', methods=['GET'])
def get_ar_info():
barcode = request.args.get('barcode')
if not barcode or barcode not in ITEM_ATTRIBUTES:
products = load_products()
if not barcode or barcode not in products:
return jsonify({"error": "Item not found"}), 404
return jsonify(ITEM_ATTRIBUTES[barcode]), 200
return jsonify(products[barcode]), 200
@app.route('/images/<path:filename>', methods=['GET'])
def serve_image(filename):
image_path = os.path.join(app.config['STATIC_FOLDER'], filename)
image_path = os.path.join(app.config['STATIC_FOLDER'], 'images', filename)
if os.path.exists(image_path):
return send_file(image_path)
return jsonify({"error": "Image not found"}), 404
@app.route('/barcodes/<path:filename>', methods=['GET'])
def serve_barcode(filename):
barcode_path = os.path.join(app.config['STATIC_FOLDER'], 'barcodes', filename)
if os.path.exists(barcode_path):
return send_file(barcode_path)
return jsonify({"error": "Barcode not found"}), 404
if __name__ == '__main__':
# Ensure the required directories exist
os.makedirs(os.path.join(app.config['STATIC_FOLDER'], 'images'), exist_ok=True)
os.makedirs(os.path.join(app.config['STATIC_FOLDER'], 'barcodes'), exist_ok=True)
# If products.json doesn't exist, create it with initial data
products_file = os.path.join(app.config['STATIC_FOLDER'], 'products.json')
if not os.path.exists(products_file):
initial_data = {
"123456": [
{"fieldName": "_id", "label": "Item ID", "value": "123456", "editable": "false", "fieldType": "TEXT"},
{"fieldName": "_price", "label": "Sale Price", "value": "$49.99", "editable": "true", "fieldType": "TEXT"},
{"fieldName": "_image", "label": "Image", "value": "/images/123456.png", "editable": "false", "fieldType": "IMAGE_URI"}
],
"789012": [
{"fieldName": "_id", "label": "Item ID", "value": "789012", "editable": "false", "fieldType": "TEXT"},
{"fieldName": "_price", "label": "Sale Price", "value": "$59.99", "editable": "true", "fieldType": "TEXT"},
{"fieldName": "_image", "label": "Image", "value": "/images/789012.png", "editable": "false", "fieldType": "IMAGE_URI"}
]
}
with open(products_file, 'w') as f:
json.dump(initial_data, f, indent=2)
app.run(port=5555, host="0.0.0.0", debug=True)

112
app_compatibility.py Normal file
View File

@@ -0,0 +1,112 @@
"""
This file is a compatibility wrapper for the main app.py
It addresses the Werkzeug import issue by patching the necessary functions
"""
import sys
# Add compatibility for url_quote before importing Flask
try:
# First try normal import
from werkzeug.urls import url_quote
except ImportError:
# If it fails, create a shim for the missing function
import werkzeug
from werkzeug.urls import quote as url_quote
# Patch the werkzeug.urls module
werkzeug.urls.url_quote = url_quote
sys.modules['werkzeug.urls'].url_quote = url_quote
# Now import Flask normally
from flask import Flask, jsonify, request, send_file, render_template, flash
import os
import json
from admin import admin_bp
app = Flask(__name__)
app.config['STATIC_FOLDER'] = './static'
app.config['SECRET_KEY'] = 'dev-key-for-demo-only'
# Register blueprints
app.register_blueprint(admin_bp)
# Load product data from products.json
def load_products():
try:
with open('static/products.json', 'r') as f:
return json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
return {}
@app.route('/')
def index():
return render_template('index.html')
@app.route('/login', methods=['GET'])
def login():
# Just return 200 for this example, assuming no auth needed
return '', 200
@app.route('/arcontentfields', methods=['GET'])
def get_ar_content_fields():
# Return a fixed set of attributes
fields = [
{"fieldName": "_id", "label": "Item ID", "editable": "false", "fieldType": "TEXT"},
{"fieldName": "_price", "label": "Sale Price", "editable": "true", "fieldType": "TEXT"},
{"fieldName": "_image", "label": "Image", "editable": "false", "fieldType": "IMAGE_URI"}
]
return jsonify(fields), 200
@app.route('/arinfo', methods=['GET'])
def get_ar_info():
barcode = request.args.get('barcode')
products = load_products()
if not barcode or barcode not in products:
return jsonify({"error": "Item not found"}), 404
return jsonify(products[barcode]), 200
@app.route('/images/<path:filename>', methods=['GET'])
def serve_image(filename):
image_path = os.path.join(app.config['STATIC_FOLDER'], 'images', filename)
if os.path.exists(image_path):
return send_file(image_path)
return jsonify({"error": "Image not found"}), 404
@app.route('/barcodes/<path:filename>', methods=['GET'])
def serve_barcode(filename):
barcode_path = os.path.join(app.config['STATIC_FOLDER'], 'barcodes', filename)
if os.path.exists(barcode_path):
return send_file(barcode_path)
return jsonify({"error": "Barcode not found"}), 404
if __name__ == '__main__':
# Print helpful information
print("=" * 80)
print("KCAP Demo Server")
print("=" * 80)
print("This version includes compatibility fixes for Werkzeug/Flask version mismatches.")
print("If you encounter dependency errors, please run: pip install -r requirements.txt")
print("=" * 80)
# Ensure the required directories exist
os.makedirs(os.path.join(app.config['STATIC_FOLDER'], 'images'), exist_ok=True)
os.makedirs(os.path.join(app.config['STATIC_FOLDER'], 'barcodes'), exist_ok=True)
# If products.json doesn't exist, create it with initial data
products_file = os.path.join(app.config['STATIC_FOLDER'], 'products.json')
if not os.path.exists(products_file):
initial_data = {
"123456": [
{"fieldName": "_id", "label": "Item ID", "value": "123456", "editable": "false", "fieldType": "TEXT"},
{"fieldName": "_price", "label": "Sale Price", "value": "$49.99", "editable": "true", "fieldType": "TEXT"},
{"fieldName": "_image", "label": "Image", "value": "/images/123456.png", "editable": "false", "fieldType": "IMAGE_URI"}
],
"789012": [
{"fieldName": "_id", "label": "Item ID", "value": "789012", "editable": "false", "fieldType": "TEXT"},
{"fieldName": "_price", "label": "Sale Price", "value": "$59.99", "editable": "true", "fieldType": "TEXT"},
{"fieldName": "_image", "label": "Image", "value": "/images/789012.png", "editable": "false", "fieldType": "IMAGE_URI"}
]
}
with open(products_file, 'w') as f:
json.dump(initial_data, f, indent=2)
app.run(port=5555, host="0.0.0.0", debug=True)

185
barcode_generator.py Normal file
View File

@@ -0,0 +1,185 @@
import os
import io
from PIL import Image, ImageDraw, ImageFont
# Try to import barcode libraries, provide fallback if not available
try:
import qrcode
QRCODE_AVAILABLE = True
except ImportError:
QRCODE_AVAILABLE = False
try:
from barcode import EAN13, Code128
from barcode.writer import ImageWriter
BARCODE_AVAILABLE = True
except ImportError:
BARCODE_AVAILABLE = False
class BarcodeGenerator:
"""
A utility class for generating QR codes and barcodes.
"""
@staticmethod
def check_dependencies():
"""
Check if all required dependencies are installed.
Returns:
dict: Status of each dependency
"""
return {
'qrcode': QRCODE_AVAILABLE,
'barcode': BARCODE_AVAILABLE,
'pillow': True # PIL/Pillow is imported directly at the top level
}
@staticmethod
def generate_qr_code(data, size=10, border=4):
"""
Generates a QR code as a PIL Image object.
Args:
data (str): The data to encode in the QR code
size (int): The size of the QR code (1-40)
border (int): The border size of the QR code
Returns:
PIL.Image: The generated QR code image or a placeholder image if qrcode is not available
"""
if not QRCODE_AVAILABLE:
return BarcodeGenerator._generate_placeholder("QR Code Unavailable\nPlease install 'qrcode' package")
qr = qrcode.QRCode(
version=size,
error_correction=qrcode.constants.ERROR_CORRECT_L,
box_size=10,
border=border,
)
qr.add_data(data)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
return img
@staticmethod
def generate_ean13_barcode(data):
"""
Generates an EAN-13 barcode as a PIL Image object.
Args:
data (str): The data to encode in the barcode (must be 12 digits)
Returns:
PIL.Image: The generated barcode image or a placeholder image if barcode is not available
"""
if not BARCODE_AVAILABLE:
return BarcodeGenerator._generate_placeholder("EAN-13 Unavailable\nPlease install 'python-barcode' package")
# Pad data to 12 digits if needed
if len(data) < 12:
data = data.zfill(12)
elif len(data) > 12:
data = data[:12]
# Create a BytesIO object to temporarily store the image
buffer = io.BytesIO()
EAN13(data, writer=ImageWriter()).write(buffer)
# Create a PIL Image from the BytesIO object
buffer.seek(0)
image = Image.open(buffer)
return image
@staticmethod
def generate_code128_barcode(data):
"""
Generates a Code 128 barcode as a PIL Image object.
Args:
data (str): The data to encode in the barcode
Returns:
PIL.Image: The generated barcode image or a placeholder image if barcode is not available
"""
if not BARCODE_AVAILABLE:
return BarcodeGenerator._generate_placeholder("Code 128 Unavailable\nPlease install 'python-barcode' package")
# Create a BytesIO object to temporarily store the image
buffer = io.BytesIO()
Code128(data, writer=ImageWriter()).write(buffer)
# Create a PIL Image from the BytesIO object
buffer.seek(0)
image = Image.open(buffer)
return image
@staticmethod
def _generate_placeholder(message):
"""
Generates a placeholder image with a message.
Args:
message (str): Message to display on the placeholder
Returns:
PIL.Image: The generated placeholder image
"""
# Create a placeholder image
img = Image.new('RGB', (300, 150), color=(255, 255, 255))
d = ImageDraw.Draw(img)
# Draw a border
d.rectangle([0, 0, 299, 149], outline=(0, 0, 0), width=2)
# Add text
try:
# Try to use a default font
font = ImageFont.truetype("arial.ttf", 20)
except IOError:
# Fallback to default font
font = ImageFont.load_default()
# Center the text
lines = message.split('\n')
y_offset = 40
for line in lines:
text_width = d.textlength(line, font=font)
d.text(((300 - text_width) // 2, y_offset), line, font=font, fill=(0, 0, 0))
y_offset += 30
return img
@staticmethod
def save_image(image, path):
"""
Saves a PIL Image to the specified path.
Args:
image (PIL.Image): The image to save
path (str): The path to save the image to
Returns:
str: The path where the image was saved
"""
image.save(path)
return path
@staticmethod
def get_image_as_bytes(image, format='PNG'):
"""
Converts a PIL Image to bytes.
Args:
image (PIL.Image): The image to convert
format (str): The format to save the image as
Returns:
bytes: The image as bytes
"""
buffer = io.BytesIO()
image.save(buffer, format=format)
return buffer.getvalue()

5
requirements.txt Normal file
View File

@@ -0,0 +1,5 @@
Flask==2.0.1
Werkzeug==2.0.3
python-barcode==0.14.0
qrcode==7.4.2
Pillow==9.5.0

View File

@@ -0,0 +1,102 @@
{% extends "layout.html" %}
{% block title %}Add New Product - KCAP Demo Server{% endblock %}
{% block content %}
<div class="row mt-4">
<div class="col-md-8 offset-md-2">
<div class="card">
<div class="card-header">
<h2>Add New Product</h2>
</div>
<div class="card-body">
<form action="{{ url_for('admin.add_product') }}" method="POST" enctype="multipart/form-data">
<div class="mb-3">
<label for="product_id" class="form-label">Product ID *</label>
<div class="input-group">
<input type="text" class="form-control" id="product_id" name="product_id" required>
<button class="btn btn-outline-secondary" type="button" id="generate-id-btn" title="Generate Random ID">
<i class="bi bi-magic"></i> Generate
</button>
</div>
<div class="form-text">Must be unique. This will be used as the barcode for AR content.</div>
</div>
<div class="mb-3">
<label for="name" class="form-label">Product Name *</label>
<input type="text" class="form-control" id="name" name="name" required>
<div class="form-text">Enter a descriptive name for the product.</div>
</div>
<div class="mb-3">
<label for="price" class="form-label">Price *</label>
<div class="input-group">
<span class="input-group-text">$</span>
<input type="text" class="form-control" id="price" name="price" required pattern="[0-9]+(\.[0-9]{1,2})?" placeholder="49.99">
</div>
<div class="form-text">Enter the price in decimal format (e.g., 49.99)</div>
</div>
<div class="mb-3">
<label for="image" class="form-label">Product Image</label>
<input type="file" class="form-control" id="image" name="image" accept="image/*" onchange="previewImage(this)">
<div class="form-text">Supported formats: .png, .jpg, .jpeg, .gif</div>
<div class="mt-2">
<img id="image-preview" class="image-preview" style="display: none;">
</div>
</div>
<div class="d-flex justify-content-between">
<a href="{{ url_for('admin.index') }}" class="btn btn-secondary">Cancel</a>
<button type="submit" class="btn btn-primary">Add Product</button>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
function previewImage(input) {
var preview = document.getElementById('image-preview');
if (input.files && input.files[0]) {
var reader = new FileReader();
reader.onload = function(e) {
preview.src = e.target.result;
preview.style.display = 'block';
}
reader.readAsDataURL(input.files[0]);
} else {
preview.style.display = 'none';
}
}
// Generate a random product ID
document.getElementById('generate-id-btn').addEventListener('click', function() {
// Generate a random alphanumeric ID
// Format: 3 letters followed by 4 numbers (e.g., ABC1234)
const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
const numbers = '0123456789';
let id = '';
// Add 3 random uppercase letters
for (let i = 0; i < 3; i++) {
id += letters.charAt(Math.floor(Math.random() * letters.length));
}
// Add 4 random numbers
for (let i = 0; i < 4; i++) {
id += numbers.charAt(Math.floor(Math.random() * numbers.length));
}
// Set the generated ID in the input field
document.getElementById('product_id').value = id;
});
</script>
{% endblock %}

View File

@@ -0,0 +1,97 @@
{% extends "layout.html" %}
{% block title %}Edit Product - KCAP Demo Server{% endblock %}
{% block content %}
<div class="row mt-4">
<div class="col-md-8 offset-md-2">
<div class="card">
<div class="card-header">
<h2>Edit Product</h2>
</div>
<div class="card-body">
<form action="{{ url_for('admin.edit_product', product_id=product_id) }}" method="POST" enctype="multipart/form-data">
<div class="mb-3">
<label for="product_id" class="form-label">Product ID</label>
<input type="text" class="form-control" id="product_id" value="{{ product_id }}" disabled>
<div class="form-text">Product ID cannot be changed.</div>
</div>
<div class="mb-3">
<label for="name" class="form-label">Product Name *</label>
{% for field in product %}
{% if field.fieldName == '_name' %}
<input type="text" class="form-control" id="name" name="name" required value="{{ field.value }}">
{% endif %}
{% endfor %}
<div class="form-text">Enter a descriptive name for the product.</div>
</div>
<div class="mb-3">
<label for="price" class="form-label">Price *</label>
<div class="input-group">
<span class="input-group-text">$</span>
{% for field in product %}
{% if field.fieldName == '_price' %}
{% set price_value = field.value|replace('$', '') %}
<input type="text" class="form-control" id="price" name="price" required
pattern="[0-9]+(\.[0-9]{1,2})?" value="{{ price_value }}">
{% endif %}
{% endfor %}
</div>
<div class="form-text">Enter the price in decimal format (e.g., 49.99)</div>
</div>
<div class="mb-3">
<label for="image" class="form-label">Product Image</label>
<input type="file" class="form-control" id="image" name="image" accept="image/*" onchange="previewImage(this)">
<div class="form-text">Leave empty to keep the current image.</div>
<div class="mt-3">
<label class="form-label">Current Image:</label>
{% for field in product %}
{% if field.fieldName == '_image' %}
<div>
<img src="{{ field.value }}" alt="Current product image" class="image-preview">
</div>
{% endif %}
{% endfor %}
</div>
<div class="mt-2">
<label class="form-label">New Image Preview:</label>
<img id="image-preview" class="image-preview" style="display: none;">
</div>
</div>
<div class="d-flex justify-content-between">
<a href="{{ url_for('admin.index') }}" class="btn btn-secondary">Cancel</a>
<button type="submit" class="btn btn-primary">Update Product</button>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
function previewImage(input) {
var preview = document.getElementById('image-preview');
if (input.files && input.files[0]) {
var reader = new FileReader();
reader.onload = function(e) {
preview.src = e.target.result;
preview.style.display = 'block';
}
reader.readAsDataURL(input.files[0]);
} else {
preview.style.display = 'none';
}
}
</script>
{% endblock %}

View File

@@ -0,0 +1,150 @@
{% extends "layout.html" %}
{% block title %}Generate Barcodes - KCAP Demo Server{% endblock %}
{% block content %}
<div class="row mt-4">
<div class="col-md-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h2>Generate Barcodes for Product: {{ product_id }}</h2>
<a href="{{ url_for('admin.index') }}" class="btn btn-outline-secondary">Back to Products</a>
</div>
<div class="card-body">
{% if not dependencies.qrcode or not dependencies.barcode or not dependencies.pillow %}
<div class="alert alert-warning mb-4">
<h4 class="alert-heading">Missing Dependencies</h4>
<p>Some barcode generation features are unavailable because required packages are not installed:</p>
<ul>
{% if not dependencies.qrcode %}
<li><strong>QR Code:</strong> The 'qrcode' package is required for QR code generation</li>
{% endif %}
{% if not dependencies.barcode %}
<li><strong>Barcodes:</strong> The 'python-barcode' package is required for EAN-13 and Code 128 generation</li>
{% endif %}
{% if not dependencies.pillow %}
<li><strong>Image Processing:</strong> The 'Pillow' package is required for image processing</li>
{% endif %}
</ul>
<hr>
<p class="mb-0">To install the required dependencies, run: <code>pip install -r requirements.txt</code></p>
</div>
{% endif %}
<div class="row">
<div class="col-md-4">
<div class="card">
<div class="card-header">
<h3>QR Code</h3>
</div>
<div class="card-body text-center">
<img src="{{ url_for('admin.generate_barcode', product_id=product_id, code_type='qrcode') }}" alt="QR Code" class="img-fluid mb-3" style="max-width: 250px;">
<p class="text-muted">QR Code contains a link to the product AR info.</p>
<a href="{{ url_for('admin.generate_barcode', product_id=product_id, code_type='qrcode') }}" class="btn btn-primary" download="product_{{ product_id }}_qrcode.png">Download QR Code</a>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
<h3>EAN-13 Barcode</h3>
</div>
<div class="card-body text-center">
<img src="{{ url_for('admin.generate_barcode', product_id=product_id, code_type='ean13') }}" alt="EAN-13 Barcode" class="img-fluid mb-3" style="max-width: 250px;">
<p class="text-muted">Standard EAN-13 barcode format.</p>
<a href="{{ url_for('admin.generate_barcode', product_id=product_id, code_type='ean13') }}" class="btn btn-primary" download="product_{{ product_id }}_ean13.png">Download EAN-13</a>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
<h3>Code 128 Barcode</h3>
</div>
<div class="card-body text-center">
<img src="{{ url_for('admin.generate_barcode', product_id=product_id, code_type='code128') }}" alt="Code 128 Barcode" class="img-fluid mb-3" style="max-width: 250px;">
<p class="text-muted">High-density alphanumeric barcode.</p>
<a href="{{ url_for('admin.generate_barcode', product_id=product_id, code_type='code128') }}" class="btn btn-primary" download="product_{{ product_id }}_code128.png">Download Code 128</a>
</div>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h3>Product Information</h3>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-4">
<p><strong>Product ID:</strong> {{ product_id }}</p>
<p><strong>Price:</strong> {{ product.price }}</p>
</div>
<div class="col-md-4">
<p><strong>AR Info URL:</strong>
<a href="{{ url_for('get_ar_info', barcode=product_id) }}" target="_blank">
{{ url_for('get_ar_info', barcode=product_id, _external=True) }}
</a>
</p>
</div>
<div class="col-md-4">
{% if product.image %}
<img src="{{ product.image }}" alt="Product Image" class="img-thumbnail" style="max-height: 100px;">
{% else %}
<p>No product image available</p>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h3>Print All Codes</h3>
</div>
<div class="card-body">
<p>Use the button below to open a printable version of all barcodes for this product.</p>
<button class="btn btn-success" onclick="window.print()">Print Barcodes</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_css %}
<style>
@media print {
.navbar, .card-header, .btn, .text-muted, .alert {
display: none;
}
.card {
border: none;
margin-bottom: 1cm;
}
.card-body {
text-align: center;
}
.row {
display: block;
}
.col-md-4 {
width: 100%;
max-width: 100%;
flex: 0 0 100%;
page-break-after: always;
}
}
</style>
{% endblock %}

121
templates/admin/index.html Normal file
View File

@@ -0,0 +1,121 @@
{% extends "layout.html" %}
{% block title %}Admin Dashboard - KCAP Demo Server{% endblock %}
{% block content %}
<div class="row mt-4">
<div class="col-md-12">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1>Product Management</h1>
<a href="{{ url_for('admin.add_product') }}" class="btn btn-primary">Add New Product</a>
</div>
{% if products %}
<table class="table table-striped">
<thead>
<tr>
<th>Product ID</th>
<th>Name</th>
<th>Price</th>
<th>Image</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for product_id, product_data in products.items() %}
<tr>
<td>{{ product_id }}</td>
<td>
{% for field in product_data %}
{% if field.fieldName == '_name' %}
{{ field.value }}
{% endif %}
{% endfor %}
</td>
<td>
{% for field in product_data %}
{% if field.fieldName == '_price' %}
{{ field.value }}
{% endif %}
{% endfor %}
</td>
<td>
{% for field in product_data %}
{% if field.fieldName == '_image' %}
<img src="{{ field.value }}" alt="Product image" style="max-height: 100px; max-width: 100px;">
{% endif %}
{% endfor %}
</td>
<td>
<div class="btn-group">
<a href="{{ url_for('admin.edit_product', product_id=product_id) }}" class="btn btn-sm btn-outline-primary">Edit</a>
<a href="{{ url_for('admin.generate_barcode_page', product_id=product_id) }}" class="btn btn-sm btn-outline-success">Barcodes</a>
<button type="button" class="btn btn-sm btn-outline-danger"
data-bs-toggle="modal" data-bs-target="#deleteModal"
data-product-id="{{ product_id }}"
data-product-name="{% for field in product_data %}{% if field.fieldName == '_name' %}{{ field.value }}{% endif %}{% endfor %}">
Delete
</button>
</div>
<form id="delete-form-{{ product_id }}" action="{{ url_for('admin.delete_product', product_id=product_id) }}" method="POST" style="display: none;">
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="alert alert-info">
No products available. Click "Add New Product" to get started.
</div>
{% endif %}
</div>
</div>
<!-- Delete Confirmation Modal -->
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-danger text-white">
<h5 class="modal-title" id="deleteModalLabel">Confirm Deletion</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>Are you sure you want to delete the product <strong id="delete-product-name"></strong>?</p>
<p class="text-danger"><i class="bi bi-exclamation-triangle-fill"></i> This action cannot be undone.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger" id="confirm-delete-btn">Delete Product</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
// Delete confirmation modal functionality
const deleteModal = document.getElementById('deleteModal');
if (deleteModal) {
deleteModal.addEventListener('show.bs.modal', function (event) {
// Button that triggered the modal
const button = event.relatedTarget;
// Extract product info from data attributes
const productId = button.getAttribute('data-product-id');
const productName = button.getAttribute('data-product-name');
// Update modal content
const modalProductName = document.getElementById('delete-product-name');
modalProductName.textContent = productName + " (ID: " + productId + ")";
// Setup the confirm button action
const confirmDeleteBtn = document.getElementById('confirm-delete-btn');
confirmDeleteBtn.onclick = function() {
document.getElementById('delete-form-' + productId).submit();
};
});
}
</script>
{% endblock %}

40
templates/index.html Normal file
View File

@@ -0,0 +1,40 @@
{% extends "layout.html" %}
{% block title %}Home - KCAP Demo Server{% endblock %}
{% block content %}
<div class="row mt-4">
<div class="col-md-12">
<div class="jumbotron">
<h1 class="display-4">KCAP Demo Server</h1>
<p class="lead">A simple Flask API for demonstrating AR content retrieval for barcode scanning applications.</p>
<hr class="my-4">
<p>This server simulates the Knox Capture API for AR overlays and includes endpoints for managing product attributes.</p>
<div class="mt-4">
<h2>API Endpoints</h2>
<ul class="list-group">
<li class="list-group-item">
<strong>Login:</strong> <code>GET /login</code>
<p>Simulates a simple login with a 200 response (no authentication required).</p>
</li>
<li class="list-group-item">
<strong>Content Fields:</strong> <code>GET /arcontentfields</code>
<p>Returns a list of available attributes (e.g., item ID, price, image URI).</p>
</li>
<li class="list-group-item">
<strong>AR Info:</strong> <code>GET /arinfo?barcode=123456</code>
<p>Returns product details for a given barcode, including image URLs.</p>
</li>
<li class="list-group-item">
<strong>Static Image Server:</strong> <code>GET /images/123456.png</code>
<p>Serves image files from the <code>static/images/</code> directory.</p>
</li>
</ul>
</div>
<div class="mt-4">
<a href="/admin" class="btn btn-primary btn-lg">Go to Admin Interface</a>
</div>
</div>
</div>
</div>
{% endblock %}

69
templates/layout.html Normal file
View File

@@ -0,0 +1,69 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}KCAP Demo Server{% endblock %}</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Bootstrap Icons -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<style>
.navbar {
margin-bottom: 20px;
}
.container {
max-width: 1200px;
}
.card {
margin-bottom: 15px;
}
.image-preview {
max-width: 200px;
max-height: 200px;
margin: 10px 0;
}
</style>
{% block extra_css %}{% endblock %}
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="/">KCAP Demo Server</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="/">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/">Admin</a>
</li>
</ul>
</div>
</div>
</nav>
<div class="container">
{% with messages = get_flashed_messages() %}
{% if messages %}
<div class="mt-3">
{% for message in messages %}
<div class="alert alert-info" role="alert">
{{ message }}
</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</div>
<!-- Bootstrap JavaScript -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
{% block extra_js %}{% endblock %}
</body>
</html>