Compare commits

...

15 Commits

Author SHA1 Message Date
jeremygan2021
972ef57337 t 2026-03-02 12:19:43 +08:00
jeremygan2021
12b8555592 t 2026-03-02 12:14:40 +08:00
jeremygan2021
c893aa6511 new 2026-03-02 12:06:26 +08:00
jeremygan2021
3cfb04f85a new 2025-12-02 16:34:25 +08:00
jeremygan2021
eb47ab4fe6 docker 动态地址调用tool 2025-11-26 21:13:34 +08:00
jeremygan2021
7236912a53 docker 2025-11-26 21:01:58 +08:00
jeremygan2021
a1872a7a33 超级sh 2025-11-26 20:27:24 +08:00
jeremygan2021
45bb0a4d41 docker 2025-11-26 19:22:05 +08:00
jeremygan2021
371d959646 public api测试 2025-11-26 18:04:15 +08:00
jeremygan2021
22aa782648 二进制图片 2025-11-26 17:29:16 +08:00
jeremygan2021
5c36736141 mqtt password 2025-11-16 18:20:08 +08:00
jeremygan2021
6dc4d0699a docker file 2025-11-16 18:10:29 +08:00
jeremygan2021
1678d67cfa 增加API鉴权 2025-11-16 18:00:31 +08:00
jeremygan2021
b7a8a86e53 增加API鉴权 2025-11-16 18:00:28 +08:00
jeremygan2021
bb04bd8fa5 todo_list 2025-11-16 17:36:42 +08:00
78 changed files with 4350 additions and 91 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

65
.dockerignore Normal file
View File

@@ -0,0 +1,65 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Virtual Environment
venv/
env/
ENV/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Git
.git/
.gitignore
# Docker
Dockerfile*
docker-compose*
.dockerignore
# Documentation
README.md
*.md
# Logs
*.log
# Environment
.env
.env.*
# Cache
.cache/
.pytest_cache/
# Temporary files
*.tmp
*.temp

13
.env
View File

@@ -1,13 +1,14 @@
# 环境变量配置文件
# 数据库配置
DATABASE_URL=postgresql://luna:123luna@121.43.104.161:6432/luna
# DATABASE_URL=postgresql://luna:123luna@121.43.104.161:6432/luna
DATABASE_URL=postgresql://luna:123luna@6.6.6.66:5432/luna
# MQTT配置
MQTT_BROKER_HOST=localhost
MQTT_BROKER_HOST=luna-mqtt
MQTT_BROKER_PORT=1883
# MQTT_USERNAME=
# MQTT_PASSWORD=
MQTT_USERNAME=luna2025
MQTT_PASSWORD=123luna2021
# 应用配置
APP_NAME=墨水屏桌面屏幕系统
@@ -26,3 +27,7 @@ INK_HEIGHT=300
SECRET_KEY=123tangledup-ai
ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=30
# 管理员配置
ADMIN_USERNAME=admin
ADMIN_PASSWORD=123456

32
.env.docker Normal file
View File

@@ -0,0 +1,32 @@
# 环境变量配置文件
# 数据库配置
# DATABASE_URL=postgresql://luna:123luna@121.43.104.161:6432/luna
DATABASE_URL=postgresql://luna:123luna@6.6.6.66:5432/luna
# MQTT配置
MQTT_BROKER_HOST=luna-mqtt
MQTT_BROKER_PORT=1883
MQTT_USERNAME=luna2025
MQTT_PASSWORD=123luna2021
# 应用配置
APP_NAME=墨水屏桌面屏幕系统
DEBUG=false
# 文件存储配置
STATIC_DIR=static
UPLOAD_DIR=static/uploads
PROCESSED_DIR=static/processed
# 墨水屏配置
INK_WIDTH=400
INK_HEIGHT=300
# 安全配置
SECRET_KEY=123tangledup-ai
ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=30
# 管理员配置
ADMIN_USERNAME=admin
ADMIN_PASSWORD=123456

View File

@@ -1,13 +1,13 @@
# 环境变量配置文件
# 数据库配置
DATABASE_URL=postgresql://luna:123luna@121.43.104.161:6432/luna_ink
# DATABASE_URL=postgresql://luna:123luna@121.43.104.161:6432/luna
DATABASE_URL=postgresql://luna:123luna@6.6.6.66:5432/luna
# MQTT配置
MQTT_BROKER_HOST=localhost
MQTT_BROKER_HOST=luna-mqtt
MQTT_BROKER_PORT=1883
# MQTT_USERNAME=
# MQTT_PASSWORD=
MQTT_USERNAME=luna2025
MQTT_PASSWORD=123luna2021
# 应用配置
APP_NAME=墨水屏桌面屏幕系统
@@ -23,6 +23,10 @@ INK_WIDTH=400
INK_HEIGHT=300
# 安全配置
SECRET_KEY=your-secret-key-change-in-production
SECRET_KEY=123tangledup-ai
ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=30
# 管理员配置
ADMIN_USERNAME=admin
ADMIN_PASSWORD=123456

39
Dockerfile Normal file
View File

@@ -0,0 +1,39 @@
# 使用官方Python基础镜像配置国内镜像源
FROM python:3.12-slim
# 设置工作目录
WORKDIR /app
# 设置环境变量
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1
# 使用国内pip镜像源
# RUN pip config set global.index-url https://mirrors.aliyun.com/pypi/simple/ && \
# pip config set global.trusted-host mirrors.aliyun.com
# 使用国内apt镜像源
RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list.d/debian.sources && \
apt-get update && \
apt-get install -y --no-install-recommends \
gcc \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
# 复制requirements文件并安装Python依赖
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 复制项目文件
COPY . .
# 创建必要的目录
RUN mkdir -p static/uploads static/processed
# 暴露端口
EXPOSE 9999
# 启动命令
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8199"]

58
README-Docker.md Normal file
View File

@@ -0,0 +1,58 @@
# Docker 部署指南
本项目已配置 Docker 和 Docker Compose可以一键部署整个应用环境。
## 快速启动
1. 确保已安装 Docker 和 Docker Compose
2. 在项目根目录执行以下命令:
```bash
# 构建并启动所有服务
docker-compose up -d
# 查看服务状态
docker-compose ps
# 查看应用日志
docker-compose logs -f luna-app
```
3. 访问应用:
- API 文档http://localhost:9999/docs
- 管理后台http://localhost:9999/admin
## 服务说明
- **luna-app**: 主应用服务,运行在 9999 端口
- **luna-mqtt**: MQTT 服务,运行在 1883 端口
注意本项目使用外部PostgreSQL数据库数据库地址已在.env.docker文件中配置。
## 停止服务
```bash
# 停止所有服务
docker-compose down
# 停止并删除数据卷(注意:这将删除所有数据)
docker-compose down -v
```
## 注意事项
1. 首次启动可能需要几分钟等待数据库初始化完成
2. 应用配置通过.env.docker文件传递该文件已配置为使用Docker内部服务名
3. 静态文件通过 volume 挂载,可以直接在宿主机修改
4. 数据持久化通过 Docker volumes 实现
5. 如需修改配置,请编辑.env.docker文件而不是.env文件
## 国内镜像源
本项目已配置使用国内镜像源,包括:
- Python 基础镜像:阿里云镜像
- pip 包源:阿里云 pip 源
- PostgreSQL阿里云镜像
- Mosquitto阿里云镜像
这样可以大大提高在国内的构建和拉取速度。

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -6,10 +6,12 @@ from typing import Optional, List
import json
import os
import secrets
from datetime import datetime
from config import settings
from database import get_db
from models import Device as DeviceModel, Content as ContentModel
from schemas import DeviceCreate, ContentCreate
from models import Device as DeviceModel, Content as ContentModel, Todo as TodoModel
from schemas import DeviceCreate, ContentCreate, TodoCreate, TodoUpdate
from image_processor import image_processor
from mqtt_manager import mqtt_manager
@@ -19,6 +21,68 @@ templates = Jinja2Templates(directory="templates")
# 创建管理后台路由
admin_router = APIRouter()
# 登录页面
@admin_router.get("/login", response_class=HTMLResponse)
async def login_page(request: Request, next: Optional[str] = None):
"""
管理员登录页面
"""
# 如果已经登录,重定向到首页
if request.session.get("authenticated"):
return RedirectResponse(url=next or "/admin/", status_code=303)
return templates.TemplateResponse("admin/login.html", {
"request": request,
"next": next
})
# 登录处理
@admin_router.post("/login", response_class=HTMLResponse)
async def login_submit(
request: Request,
username: str = Form(...),
password: str = Form(...),
remember: Optional[bool] = Form(False),
next: Optional[str] = None
):
"""
处理管理员登录
"""
# 验证用户名和密码
# 这里使用配置文件中的设置,实际项目中应该使用数据库存储用户信息
if username == settings.admin_username and password == settings.admin_password:
# 设置会话
request.session["authenticated"] = True
request.session["username"] = username
# 设置会话过期时间
if remember:
request.session["expire_at_browser_close"] = False
else:
request.session["expire_at_browser_close"] = True
# 重定向到原始请求的页面或首页
return RedirectResponse(url=next or "/admin/", status_code=303)
else:
# 登录失败,返回错误信息
return templates.TemplateResponse("admin/login.html", {
"request": request,
"next": next,
"error": "用户名或密码错误"
})
# 登出
@admin_router.get("/logout", response_class=HTMLResponse)
async def logout(request: Request):
"""
管理员登出
"""
# 清除会话
request.session.clear()
# 重定向到登录页面
return RedirectResponse(url="/admin/login", status_code=303)
# 管理后台路由
@admin_router.get("/", response_class=HTMLResponse)
async def admin_dashboard(request: Request, db: Session = Depends(get_db)):
@@ -31,22 +95,33 @@ async def admin_dashboard(request: Request, db: Session = Depends(get_db)):
# 获取内容数量
content_count = db.query(ContentModel).count()
active_content_count = db.query(ContentModel).filter(ContentModel.is_active == True).count()
# 获取待办事项数量
todo_count = db.query(TodoModel).count()
completed_todo_count = db.query(TodoModel).filter(TodoModel.is_completed == True).count()
pending_todo_count = todo_count - completed_todo_count
# 获取最近上线的设备
recent_devices = db.query(DeviceModel).order_by(DeviceModel.last_online.desc()).limit(5).all()
# 获取最近创建的内容
recent_contents = db.query(ContentModel).order_by(ContentModel.created_at.desc()).limit(5).all()
# 获取最近创建的待办事项
recent_todos_query = db.query(TodoModel, DeviceModel).join(
DeviceModel, TodoModel.device_id == DeviceModel.device_id
).order_by(TodoModel.created_at.desc()).limit(5).all()
# 转换为包含todo和device的对象列表
recent_todos = [{"todo": todo, "device": device} for todo, device in recent_todos_query]
return templates.TemplateResponse("admin/dashboard.html", {
"request": request,
"device_count": device_count,
"active_device_count": active_device_count,
"content_count": content_count,
"active_content_count": active_content_count,
"todo_count": todo_count,
"completed_todo_count": completed_todo_count,
"pending_todo_count": pending_todo_count,
"recent_devices": recent_devices,
"recent_contents": recent_contents
"recent_todos": recent_todos
})
@admin_router.get("/devices", response_class=HTMLResponse)
@@ -342,3 +417,261 @@ async def upload_image(request: Request, db: Session = Depends(get_db)):
"devices": devices,
"error": f"图片处理失败: {str(e)}"
})
# 待办事项管理路由
@admin_router.get("/todos", response_class=HTMLResponse)
async def todos_list(request: Request, device_id: Optional[str] = None, db: Session = Depends(get_db)):
"""
待办事项列表页面
"""
if device_id:
# 获取特定设备的待办事项
device = db.query(DeviceModel).filter(DeviceModel.device_id == device_id).first()
if not device:
raise HTTPException(status_code=404, detail="设备不存在")
todos = db.query(TodoModel).filter(TodoModel.device_id == device_id).order_by(TodoModel.created_at.desc()).all()
return templates.TemplateResponse("admin/todos.html", {
"request": request,
"todos": todos,
"device": device,
"filtered": True
})
else:
# 获取所有待办事项
todos = db.query(TodoModel).order_by(TodoModel.created_at.desc()).all()
devices = db.query(DeviceModel).all()
# 为每个待办事项添加设备信息
todo_list = []
for todo in todos:
device = db.query(DeviceModel).filter(DeviceModel.device_id == todo.device_id).first()
todo_list.append({
"todo": todo,
"device": device
})
return templates.TemplateResponse("admin/todos.html", {
"request": request,
"todo_list": todo_list,
"devices": devices,
"filtered": False
})
@admin_router.get("/todos/add", response_class=HTMLResponse)
@admin_router.post("/todos/add", response_class=HTMLResponse)
async def add_todo(request: Request, device_id: Optional[str] = None, db: Session = Depends(get_db)):
"""
添加待办事项页面和处理
"""
if request.method == "GET":
devices = db.query(DeviceModel).filter(DeviceModel.is_active == True).all()
return templates.TemplateResponse("admin/todo_add.html", {
"request": request,
"devices": devices,
"selected_device": device_id
})
# 处理POST请求
form = await request.form()
device_id = form.get("device_id")
title = form.get("title")
description = form.get("description")
due_date_str = form.get("due_date")
# 检查设备是否存在
device = db.query(DeviceModel).filter(DeviceModel.device_id == device_id).first()
if not device:
devices = db.query(DeviceModel).filter(DeviceModel.is_active == True).all()
return templates.TemplateResponse("admin/todo_add.html", {
"request": request,
"devices": devices,
"error": "设备不存在"
})
# 处理截止日期
due_date = None
if due_date_str:
try:
from datetime import datetime
due_date = datetime.strptime(due_date_str, "%Y-%m-%dT%H:%M")
except ValueError:
devices = db.query(DeviceModel).filter(DeviceModel.is_active == True).all()
return templates.TemplateResponse("admin/todo_add.html", {
"request": request,
"devices": devices,
"error": "截止日期格式不正确"
})
# 创建新的待办事项
new_todo = TodoModel(
title=title,
description=description,
device_id=device_id,
due_date=due_date
)
db.add(new_todo)
db.commit()
# 发送MQTT通知给设备
mqtt_manager.send_todo_command(device_id, "create", {
"id": new_todo.id,
"title": title,
"description": description,
"due_date": due_date.isoformat() if due_date else None
})
return RedirectResponse(url="/admin/todos", status_code=303)
@admin_router.get("/todos/{todo_id}", response_class=HTMLResponse)
async def todo_detail(request: Request, todo_id: int, db: Session = Depends(get_db)):
"""
待办事项详情页面
"""
todo = db.query(TodoModel).filter(TodoModel.id == todo_id).first()
if not todo:
raise HTTPException(status_code=404, detail="待办事项不存在")
# 获取设备信息
device = db.query(DeviceModel).filter(DeviceModel.device_id == todo.device_id).first()
return templates.TemplateResponse("admin/todo_detail.html", {
"request": request,
"todo": todo,
"device": device
})
@admin_router.post("/todos/{todo_id}/toggle", response_class=HTMLResponse)
async def toggle_todo_status(todo_id: int, db: Session = Depends(get_db)):
"""
切换待办事项完成状态
"""
todo = db.query(TodoModel).filter(TodoModel.id == todo_id).first()
if not todo:
raise HTTPException(status_code=404, detail="待办事项不存在")
# 切换状态
todo.is_completed = not todo.is_completed
if todo.is_completed:
todo.completed_at = datetime.utcnow()
else:
todo.completed_at = None
todo.updated_at = datetime.utcnow()
db.commit()
# 发送MQTT通知给设备
mqtt_manager.send_todo_command(todo.device_id, "update", {
"id": todo.id,
"is_completed": todo.is_completed,
"completed_at": todo.completed_at.isoformat() if todo.completed_at else None
})
return RedirectResponse(url=f"/admin/todos/{todo_id}", status_code=303)
@admin_router.get("/todos/{todo_id}/edit", response_class=HTMLResponse)
async def edit_todo_page(request: Request, todo_id: int, db: Session = Depends(get_db)):
todo = db.query(TodoModel).filter(TodoModel.id == todo_id).first()
if not todo:
raise HTTPException(status_code=404, detail="待办事项不存在")
devices = db.query(DeviceModel).all()
return templates.TemplateResponse("admin/todo_edit.html", {
"request": request,
"todo": todo,
"devices": devices
})
@admin_router.post("/todos/{todo_id}/edit")
async def edit_todo(
request: Request,
todo_id: int,
title: str = Form(...),
description: str = Form(""),
device_id: str = Form(...),
due_date: Optional[str] = Form(None),
is_completed: bool = Form(False),
db: Session = Depends(get_db)
):
todo = db.query(TodoModel).filter(TodoModel.id == todo_id).first()
if not todo:
raise HTTPException(status_code=404, detail="待办事项不存在")
device = db.query(DeviceModel).filter(DeviceModel.device_id == device_id).first()
if not device:
raise HTTPException(status_code=404, detail="设备不存在")
# 更新待办事项
todo.title = title
todo.description = description if description else None
todo.device_id = device_id
todo.due_date = datetime.fromisoformat(due_date) if due_date else None
# 检查完成状态变化
was_completed = todo.is_completed
todo.is_completed = is_completed
# 如果从未完成变为已完成,设置完成时间
if not was_completed and is_completed:
todo.completed_at = datetime.utcnow()
# 如果从已完成变为未完成,清除完成时间
elif was_completed and not is_completed:
todo.completed_at = None
todo.updated_at = datetime.utcnow()
db.commit()
# 发送MQTT通知到设备
if hasattr(request.app.state, 'mqtt_manager') and request.app.state.mqtt_manager:
try:
await request.app.state.mqtt_manager.send_todo_command(
device_id=device_id,
action="update",
todo_data={
"id": todo.id,
"title": todo.title,
"description": todo.description,
"due_date": todo.due_date.isoformat() if todo.due_date else None,
"is_completed": todo.is_completed
}
)
except Exception as e:
print(f"发送待办事项更新MQTT消息失败: {e}")
return RedirectResponse(url=f"/admin/todos/{todo_id}", status_code=303)
@admin_router.post("/todos/{todo_id}/delete")
async def delete_todo(
request: Request,
todo_id: int,
db: Session = Depends(get_db)
):
"""
删除待办事项
"""
todo = db.query(TodoModel).filter(TodoModel.id == todo_id).first()
if not todo:
raise HTTPException(status_code=404, detail="待办事项不存在")
device_id = todo.device_id
# 发送MQTT通知到设备
if hasattr(request.app.state, 'mqtt_manager') and request.app.state.mqtt_manager:
try:
await request.app.state.mqtt_manager.send_todo_command(
device_id=device_id,
action="delete",
todo_data={
"id": todo.id
}
)
except Exception as e:
print(f"发送待办事项删除MQTT消息失败: {e}")
db.delete(todo)
db.commit()
return RedirectResponse(url=f"/admin/todos?device_id={device_id}", status_code=303)

View File

@@ -1,8 +1,9 @@
from fastapi import APIRouter
from api import devices, contents
from api import devices, contents, todos
api_router = APIRouter()
# 注册所有路由
api_router.include_router(devices.router)
api_router.include_router(contents.router)
# 注册所有路由,并添加全局安全要求
api_router.include_router(devices.router, prefix="/devices")
api_router.include_router(contents.router, prefix="/contents")
api_router.include_router(todos.router, prefix="/todos")

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,9 +1,12 @@
from fastapi import APIRouter, Depends, HTTPException, status, Query, File, UploadFile
from fastapi import APIRouter, Depends, HTTPException, status, Query, File, UploadFile, Security, Response
from sqlalchemy.orm import Session
from sqlalchemy import func
from typing import List, Optional
import json
import os
import sys
import tempfile
import importlib.util
from database import get_db
from schemas import Content as ContentSchema, ContentCreate, ContentUpdate, ContentResponse
@@ -11,13 +14,13 @@ from models import Content as ContentModel, Device as DeviceModel
from mqtt_manager import mqtt_manager
from image_processor import image_processor
from config import settings
from auth import get_api_key
router = APIRouter(
prefix="/api",
tags=["contents"]
)
@router.post("/devices/{device_id}/content", response_model=ContentSchema, status_code=status.HTTP_201_CREATED)
@router.post("/devices/{device_id}/content", response_model=ContentSchema, status_code=status.HTTP_201_CREATED, dependencies=[Depends(get_api_key)])
async def create_content(
device_id: str,
content: ContentCreate,
@@ -60,7 +63,7 @@ async def create_content(
return db_content
@router.get("/devices/{device_id}/content", response_model=List[ContentSchema])
@router.get("/devices/{device_id}/content", response_model=List[ContentSchema], dependencies=[Depends(get_api_key)])
async def list_content(
device_id: str,
skip: int = 0,
@@ -87,7 +90,7 @@ async def list_content(
contents = query.order_by(ContentModel.version.desc()).offset(skip).limit(limit).all()
return contents
@router.get("/devices/{device_id}/content/{version}", response_model=ContentResponse)
@router.get("/devices/{device_id}/content/{version}", response_model=ContentResponse, dependencies=[Depends(get_api_key)])
async def get_content(
device_id: str,
version: int,
@@ -141,7 +144,7 @@ async def get_content(
created_at=content.created_at
)
@router.put("/devices/{device_id}/content/{version}", response_model=ContentSchema)
@router.put("/devices/{device_id}/content/{version}", response_model=ContentSchema, dependencies=[Depends(get_api_key)])
async def update_content(
device_id: str,
version: int,
@@ -176,7 +179,7 @@ async def update_content(
return content
@router.delete("/devices/{device_id}/content/{version}", status_code=status.HTTP_204_NO_CONTENT)
@router.delete("/devices/{device_id}/content/{version}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(get_api_key)])
async def delete_content(
device_id: str,
version: int,
@@ -250,7 +253,7 @@ async def get_latest_content(device_id: str, db: Session = Depends(get_db)):
created_at=content.created_at
)
@router.post("/upload")
@router.post("/upload", dependencies=[Depends(get_api_key)])
async def upload_image(
device_id: str = Query(..., description="设备ID"),
version: Optional[int] = Query(None, description="内容版本,如果提供则更新指定版本"),
@@ -337,3 +340,332 @@ async def upload_image(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"图片处理失败: {str(e)}"
)
def convert_to_binary_data(image_path: str, width: int = 400, height: int = 300, invert: bool = False, rotate: bool = False, dither: bool = True) -> bytes:
"""
使用image_converter.py工具将图片转换为二进制数据
Args:
image_path: 图片路径
width: 目标宽度
height: 目标高度
invert: 是否反转颜色
rotate: 是否旋转90度
dither: 是否使用抖动算法
Returns:
二进制数据
"""
try:
# 动态导入image_converter模块 - 使用相对路径
import os
# 获取当前文件的目录
current_dir = os.path.dirname(os.path.abspath(__file__))
# 构建到tool目录的路径
tool_dir = os.path.join(current_dir, "..", "tool")
image_converter_path = os.path.join(tool_dir, "image_converter.py")
# 导入模块
spec = importlib.util.spec_from_file_location("image_converter", image_converter_path)
image_converter = importlib.util.module_from_spec(spec)
spec.loader.exec_module(image_converter)
# 创建临时文件
with tempfile.NamedTemporaryFile(suffix='.py', delete=False) as temp_file:
temp_path = temp_file.name
try:
# 使用image_converter转换图片
image_converter.convert_image_to_epaper(
image_path,
temp_path,
width=width,
height=height,
invert=invert,
rotate=rotate,
dither=dither
)
# 读取生成的Python文件并提取二进制数据
with open(temp_path, 'r', encoding='utf-8') as f:
content = f.read()
# 解析二进制数据
start_idx = content.find("bytearray(b'") + len("bytearray(b'")
end_idx = content.find("')", start_idx)
# 提取并解析十六进制字符串
hex_str = content[start_idx:end_idx]
# 替换换行符和空格
hex_str = hex_str.replace("'\n b'", "")
# 转换为字节数组
binary_data = bytearray()
i = 0
while i < len(hex_str):
if hex_str[i] == '\\' and i + 1 < len(hex_str) and hex_str[i+1] == 'x':
# 提取十六进制值
hex_val = hex_str[i+2:i+4]
binary_data.append(int(hex_val, 16))
i += 4
else:
i += 1
return bytes(binary_data)
finally:
# 删除临时文件
if os.path.exists(temp_path):
os.unlink(temp_path)
except Exception as e:
raise Exception(f"图片转换为二进制数据失败: {str(e)}")
@router.get("/devices/{device_id}/content/latest/binary", dependencies=[Depends(get_api_key)])
async def get_latest_content_binary(
device_id: str,
invert: bool = Query(False, description="是否反转颜色"),
rotate: bool = Query(False, description="是否旋转90度"),
dither: bool = Query(True, description="是否使用抖动算法"),
db: Session = Depends(get_db)
):
"""
获取设备最新活跃内容的二进制数据,适用于墨水屏显示
"""
# 检查设备是否存在
device = db.query(DeviceModel).filter(DeviceModel.device_id == device_id).first()
if not device:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="设备不存在"
)
# 获取最新的活跃内容
content = db.query(ContentModel).filter(
ContentModel.device_id == device_id,
ContentModel.is_active == True
).order_by(ContentModel.version.desc()).first()
if not content:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="设备没有活跃内容"
)
if not content.image_path:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="内容没有关联的图片"
)
try:
# 获取完整的图片路径
# 检查image_path是否已经包含static目录
if content.image_path.startswith('static/'):
# 已经包含static目录直接使用
image_path = content.image_path
elif content.image_path.startswith('/'):
# 以/开头的完整路径,去掉开头的斜杠
image_path = content.image_path[1:]
else:
# 相对路径添加static_dir前缀
image_path = os.path.join(settings.static_dir, content.image_path)
# 确保图片文件存在
if not os.path.exists(image_path):
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="图片文件不存在"
)
# 转换为二进制数据
binary_data = convert_to_binary_data(
image_path,
width=settings.ink_width,
height=settings.ink_height,
invert=invert,
rotate=rotate,
dither=dither
)
# 返回二进制数据
return Response(
content=binary_data,
media_type="application/octet-stream",
headers={
"Content-Disposition": f"attachment; filename={device_id}_latest.bin",
"Content-Length": str(len(binary_data))
}
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"生成二进制数据失败: {str(e)}"
)
@router.get("/public/devices/{device_id}/content/latest/binary")
async def get_latest_content_binary_public(
device_id: str,
invert: bool = Query(False, description="是否反转颜色"),
rotate: bool = Query(False, description="是否旋转90度"),
dither: bool = Query(True, description="是否使用抖动算法"),
db: Session = Depends(get_db)
):
"""
获取设备最新活跃内容的二进制数据,适用于墨水屏显示(无需鉴权)
"""
# 检查设备是否存在
device = db.query(DeviceModel).filter(DeviceModel.device_id == device_id).first()
if not device:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="设备不存在"
)
# 获取最新的活跃内容
content = db.query(ContentModel).filter(
ContentModel.device_id == device_id,
ContentModel.is_active == True
).order_by(ContentModel.version.desc()).first()
if not content:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="设备没有活跃内容"
)
if not content.image_path:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="内容没有关联的图片"
)
try:
# 获取完整的图片路径
# 检查image_path是否已经包含static目录
if content.image_path.startswith('static/'):
# 已经包含static目录直接使用
image_path = content.image_path
elif content.image_path.startswith('/'):
# 以/开头的完整路径,去掉开头的斜杠
image_path = content.image_path[1:]
else:
# 相对路径添加static_dir前缀
image_path = os.path.join(settings.static_dir, content.image_path)
# 确保图片文件存在
if not os.path.exists(image_path):
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="图片文件不存在"
)
# 转换为二进制数据
binary_data = convert_to_binary_data(
image_path,
width=settings.ink_width,
height=settings.ink_height,
invert=invert,
rotate=rotate,
dither=dither
)
# 返回二进制数据
return Response(
content=binary_data,
media_type="application/octet-stream",
headers={
"Content-Disposition": f"attachment; filename={device_id}_latest.bin",
"Content-Length": str(len(binary_data))
}
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"生成二进制数据失败: {str(e)}"
)
@router.get("/devices/{device_id}/content/{version}/binary", dependencies=[Depends(get_api_key)])
async def get_content_binary(
device_id: str,
version: int,
invert: bool = Query(False, description="是否反转颜色"),
rotate: bool = Query(False, description="是否旋转90度"),
dither: bool = Query(True, description="是否使用抖动算法"),
db: Session = Depends(get_db)
):
"""
获取内容的二进制数据,适用于墨水屏显示
"""
# 检查设备是否存在
device = db.query(DeviceModel).filter(DeviceModel.device_id == device_id).first()
if not device:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="设备不存在"
)
# 获取内容
content = db.query(ContentModel).filter(
ContentModel.device_id == device_id,
ContentModel.version == version
).first()
if not content:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="内容不存在"
)
if not content.image_path:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="内容没有关联的图片"
)
try:
# 获取完整的图片路径
# 检查image_path是否已经包含static目录
if content.image_path.startswith('static/'):
# 已经包含static目录直接使用
image_path = content.image_path
elif content.image_path.startswith('/'):
# 以/开头的完整路径,去掉开头的斜杠
image_path = content.image_path[1:]
else:
# 相对路径添加static_dir前缀
image_path = os.path.join(settings.static_dir, content.image_path)
# 确保图片文件存在
if not os.path.exists(image_path):
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="图片文件不存在"
)
# 转换为二进制数据
binary_data = convert_to_binary_data(
image_path,
width=settings.ink_width,
height=settings.ink_height,
invert=invert,
rotate=rotate,
dither=dither
)
# 返回二进制数据
return Response(
content=binary_data,
media_type="application/octet-stream",
headers={
"Content-Disposition": f"attachment; filename={device_id}_v{version}.bin",
"Content-Length": str(len(binary_data))
}
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"生成二进制数据失败: {str(e)}"
)

View File

@@ -1,4 +1,4 @@
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi import APIRouter, Depends, HTTPException, status, Security
from sqlalchemy.orm import Session
from typing import List, Optional
from datetime import datetime
@@ -9,13 +9,13 @@ from schemas import Device as DeviceSchema, DeviceCreate, DeviceUpdate, Bootstra
from models import Device as DeviceModel
from database import Content as ContentModel
from mqtt_manager import mqtt_manager
from auth import get_api_key
router = APIRouter(
prefix="/api/devices",
tags=["devices"]
)
@router.post("/", response_model=DeviceSchema, status_code=status.HTTP_201_CREATED)
@router.post("/", response_model=DeviceSchema, status_code=status.HTTP_201_CREATED, dependencies=[Depends(get_api_key)])
async def create_device(device: DeviceCreate, db: Session = Depends(get_db)):
"""
注册新设备
@@ -46,7 +46,7 @@ async def create_device(device: DeviceCreate, db: Session = Depends(get_db)):
return db_device
@router.get("/", response_model=List[DeviceSchema])
@router.get("/", response_model=List[DeviceSchema], dependencies=[Depends(get_api_key)])
async def list_devices(
skip: int = 0,
limit: int = 100,
@@ -68,7 +68,7 @@ async def list_devices(
devices = query.offset(skip).limit(limit).all()
return devices
@router.get("/{device_id}", response_model=DeviceSchema)
@router.get("/{device_id}", response_model=DeviceSchema, dependencies=[Depends(get_api_key)])
async def get_device(device_id: str, db: Session = Depends(get_db)):
"""
获取设备详情
@@ -81,7 +81,7 @@ async def get_device(device_id: str, db: Session = Depends(get_db)):
)
return device
@router.put("/{device_id}", response_model=DeviceSchema)
@router.put("/{device_id}", response_model=DeviceSchema, dependencies=[Depends(get_api_key)])
async def update_device(
device_id: str,
device_update: DeviceUpdate,
@@ -108,7 +108,7 @@ async def update_device(
return device
@router.delete("/{device_id}", status_code=status.HTTP_204_NO_CONTENT)
@router.delete("/{device_id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(get_api_key)])
async def delete_device(device_id: str, db: Session = Depends(get_db)):
"""
删除设备
@@ -126,11 +126,12 @@ async def delete_device(device_id: str, db: Session = Depends(get_db)):
db.delete(device)
db.commit()
@router.get("/{device_id}/bootstrap", response_model=BootstrapResponse)
async def device_bootstrap(device_id: str, db: Session = Depends(get_db)):
@router.post("/{device_id}/bootstrap", response_model=BootstrapResponse, dependencies=[Depends(get_api_key)])
async def bootstrap_device(device_id: str, db: Session = Depends(get_db)):
"""
设备启动获取当前版本信息
设备引导 - 获取设备配置和内容
"""
# 验证设备是否存在
device = db.query(DeviceModel).filter(DeviceModel.device_id == device_id).first()
if not device:
raise HTTPException(
@@ -138,29 +139,19 @@ async def device_bootstrap(device_id: str, db: Session = Depends(get_db)):
detail="设备不存在"
)
# 更新设备最后在线时间
device.last_online = datetime.utcnow()
db.commit()
# 获取最新的活跃内容
latest_content = db.query(ContentModel).filter(
ContentModel.device_id == device_id,
ContentModel.is_active == True
).order_by(ContentModel.version.desc()).first()
# 获取设备场景的内容
contents = db.query(ContentModel).filter(ContentModel.scene == device.scene).all()
# 构建响应
response = BootstrapResponse(
device_id=device_id,
timezone=latest_content.timezone if latest_content else "Asia/Shanghai",
time_format=latest_content.time_format if latest_content else "%Y-%m-%d %H:%M"
scene=device.scene,
contents=contents
)
if latest_content:
response.content_version = latest_content.version
response.last_updated = latest_content.created_at
return response
@router.get("/{device_id}/status")
@router.get("/{device_id}/status", dependencies=[Depends(get_api_key)])
async def get_device_status(device_id: str, db: Session = Depends(get_db)):
"""
获取设备状态

168
api/todos.py Normal file
View File

@@ -0,0 +1,168 @@
from fastapi import APIRouter, Depends, HTTPException, status, Security
from sqlalchemy.orm import Session
from typing import List, Optional
from datetime import datetime
from database import get_db
from schemas import Todo as TodoSchema, TodoCreate, TodoUpdate
from models import Todo as TodoModel
from database import Device as DeviceModel
from auth import get_api_key
router = APIRouter(
tags=["todos"]
)
@router.post("/", response_model=TodoSchema, status_code=status.HTTP_201_CREATED, dependencies=[Depends(get_api_key)])
async def create_todo(todo: TodoCreate, db: Session = Depends(get_db)):
"""
创建新的待办事项
"""
# 检查设备是否存在
device = db.query(DeviceModel).filter(DeviceModel.device_id == todo.device_id).first()
if not device:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="设备不存在"
)
# 创建新的待办事项
db_todo = TodoModel(
title=todo.title,
description=todo.description,
device_id=todo.device_id,
due_date=todo.due_date
)
db.add(db_todo)
db.commit()
db.refresh(db_todo)
return db_todo
@router.get("/", response_model=List[TodoSchema], dependencies=[Depends(get_api_key)])
async def list_todos(
skip: int = 0,
limit: int = 100,
device_id: Optional[str] = None,
is_completed: Optional[bool] = None,
db: Session = Depends(get_db)
):
"""
获取待办事项列表
"""
query = db.query(TodoModel)
if device_id:
query = query.filter(TodoModel.device_id == device_id)
if is_completed is not None:
query = query.filter(TodoModel.is_completed == is_completed)
todos = query.order_by(TodoModel.created_at.desc()).offset(skip).limit(limit).all()
return todos
@router.get("/{todo_id}", response_model=TodoSchema, dependencies=[Depends(get_api_key)])
async def get_todo(todo_id: int, db: Session = Depends(get_db)):
"""
获取待办事项详情
"""
todo = db.query(TodoModel).filter(TodoModel.id == todo_id).first()
if not todo:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="待办事项不存在"
)
return todo
@router.put("/{todo_id}", response_model=TodoSchema, dependencies=[Depends(get_api_key)])
async def update_todo(
todo_id: int,
todo_update: TodoUpdate,
db: Session = Depends(get_db)
):
"""
更新待办事项
"""
todo = db.query(TodoModel).filter(TodoModel.id == todo_id).first()
if not todo:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="待办事项不存在"
)
# 更新待办事项信息
update_data = todo_update.model_dump(exclude_unset=True)
# 如果状态从未完成变为完成,设置完成时间
if "is_completed" in update_data and update_data["is_completed"] and not todo.is_completed:
update_data["completed_at"] = datetime.utcnow()
# 如果状态从完成变为未完成,清除完成时间
elif "is_completed" in update_data and not update_data["is_completed"] and todo.is_completed:
update_data["completed_at"] = None
for field, value in update_data.items():
setattr(todo, field, value)
todo.updated_at = datetime.utcnow()
db.commit()
db.refresh(todo)
return todo
@router.delete("/{todo_id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(get_api_key)])
async def delete_todo(todo_id: int, db: Session = Depends(get_db)):
"""
删除待办事项
"""
todo = db.query(TodoModel).filter(TodoModel.id == todo_id).first()
if not todo:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="待办事项不存在"
)
db.delete(todo)
db.commit()
@router.post("/{todo_id}/complete", response_model=TodoSchema, dependencies=[Depends(get_api_key)])
async def complete_todo(todo_id: int, db: Session = Depends(get_db)):
"""
标记待办事项为完成
"""
todo = db.query(TodoModel).filter(TodoModel.id == todo_id).first()
if not todo:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="待办事项不存在"
)
todo.is_completed = True
todo.completed_at = datetime.utcnow()
todo.updated_at = datetime.utcnow()
db.commit()
db.refresh(todo)
return todo
@router.post("/{todo_id}/incomplete", response_model=TodoSchema, dependencies=[Depends(get_api_key)])
async def incomplete_todo(todo_id: int, db: Session = Depends(get_db)):
"""
标记待办事项为未完成
"""
todo = db.query(TodoModel).filter(TodoModel.id == todo_id).first()
if not todo:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="待办事项不存在"
)
todo.is_completed = False
todo.completed_at = None
todo.updated_at = datetime.utcnow()
db.commit()
db.refresh(todo)
return todo

128
auth.py Normal file
View File

@@ -0,0 +1,128 @@
from fastapi import HTTPException, status, Request, Security, Depends
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials, APIKeyHeader
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import Response
from config import settings
import logging
logger = logging.getLogger(__name__)
# 创建API Key安全方案
api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
async def get_api_key(api_key: str = Security(api_key_header)):
"""
API Key依赖项用于路由级别的鉴权
"""
if not api_key:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="缺少API Key",
headers={"WWW-Authenticate": "ApiKey"},
)
if api_key != settings.secret_key:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="无效的API Key",
headers={"WWW-Authenticate": "ApiKey"},
)
return api_key
class APIKeyMiddleware(BaseHTTPMiddleware):
"""
API Key鉴权中间件
验证请求中的Secret Key
"""
async def dispatch(self, request: Request, call_next):
# 跳过不需要鉴权的路径
if self._should_skip_auth(request.url.path):
return await call_next(request)
# 检查API Key
api_key = request.headers.get("X-API-Key")
if not api_key:
logger.warning(f"缺少API Key: {request.method} {request.url.path}")
return Response(
content='{"detail": "缺少API Key"}',
status_code=status.HTTP_401_UNAUTHORIZED,
media_type="application/json"
)
# 验证API Key
if api_key != settings.secret_key:
logger.warning(f"无效的API Key: {request.method} {request.url.path}")
return Response(
content='{"detail": "无效的API Key"}',
status_code=status.HTTP_401_UNAUTHORIZED,
media_type="application/json"
)
return await call_next(request)
def _should_skip_auth(self, path: str) -> bool:
"""
判断是否跳过鉴权的路径
"""
# 公共API路径不需要鉴权
public_api_paths = [
"/api/contents/public/",
]
# 检查是否是公共API路径
for public_path in public_api_paths:
if path.startswith(public_path):
return True
# 所有其他API路径都需要鉴权不跳过
# 如果路径以/api开头则不跳过需要鉴权
if path.startswith("/api"):
return False
skip_paths = [
"/",
"/health",
"/docs",
"/redoc",
"/openapi.json",
"/admin",
"/admin/login",
"/static",
]
# 检查是否以跳过路径开头
for skip_path in skip_paths:
if path.startswith(skip_path):
return True
return False
class AdminAuthMiddleware(BaseHTTPMiddleware):
"""
Admin页面认证中间件
验证用户是否已登录
"""
async def dispatch(self, request: Request, call_next):
# 只对admin路径进行认证
if not request.url.path.startswith("/admin") or request.url.path == "/admin/login":
return await call_next(request)
# 检查会话
if not self._is_authenticated(request):
# 重定向到登录页面
from fastapi.responses import RedirectResponse
return RedirectResponse(url="/admin/login?next=" + request.url.path, status_code=303)
return await call_next(request)
def _is_authenticated(self, request: Request) -> bool:
"""
检查用户是否已认证
"""
# 从session中获取认证信息
session = request.session
return session.get("authenticated", False)

View File

@@ -2,8 +2,10 @@ from pydantic_settings import BaseSettings
from typing import Optional
class Settings(BaseSettings):
# 数据库配置
database_url: str = "postgresql://luna:123luna@121.43.104.161:6432/luna_ink"
# 数据库配置1
#database_url: str = "postgresql://luna:123luna@121.43.104.161:6432/luna"
database_url: str = "postgresql://luna:123luna@6.6.6.66:5432/luna"
# MQTT配置
mqtt_broker_host: str = "localhost"
@@ -25,11 +27,16 @@ class Settings(BaseSettings):
ink_height: int = 300
# 安全配置
secret_key: str = "your-secret-key-change-in-production"
secret_key: str = "123tangledup-ai"
algorithm: str = "HS256"
access_token_expire_minutes: int = 30
# 管理员配置
admin_username: str = "admin"
admin_password: str = "123456"
class Config:
env_file = ".env"
case_sensitive = False
settings = Settings()

View File

@@ -40,6 +40,22 @@ class Content(Base):
# 关联设备
device = relationship("Device", back_populates="contents")
class Todo(Base):
__tablename__ = "todos"
id = Column(Integer, primary_key=True, index=True)
title = Column(String(200), nullable=False)
description = Column(Text, nullable=True)
device_id = Column(String(50), ForeignKey("devices.device_id"), nullable=False)
is_completed = Column(Boolean, default=False)
due_date = Column(DateTime, nullable=True)
completed_at = Column(DateTime, nullable=True)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# 关联设备
device = relationship("Device")
# 创建数据库连接
engine = create_engine(settings.database_url)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

45
docker-compose.yml Normal file
View File

@@ -0,0 +1,45 @@
services:
# 主应用服务
luna-app:
build:
context: .
dockerfile: Dockerfile
container_name: luna-app
ports:
- "8199:8199"
volumes:
- ./static:/app/static
env_file:
- .env.docker
depends_on:
- luna-mqtt
restart: unless-stopped
networks:
- luna-network
# MQTT服务 (使用Eclipse Mosquitto)
luna-mqtt:
image: eclipse-mosquitto:2.0
container_name: luna-mqtt
ports:
- "1883:1883"
- "9001:9001"
volumes:
- ./mosquitto.conf:/mosquitto/config/mosquitto.conf
- mosquitto_data:/mosquitto/data
- mosquitto_logs:/mosquitto/log
- mosquitto_config:/mosquitto/config
restart: unless-stopped
networks:
- luna-network
# 数据卷
volumes:
mosquitto_data:
mosquitto_logs:
mosquitto_config:
# 网络
networks:
luna-network:
driver: bridge

292
docker_deplay.sh Executable file
View File

@@ -0,0 +1,292 @@
#!/bin/bash
# =============================================================================
# Docker 镜像构建和部署自动化脚本
# chmod u+x docker_deplay.sh
# 用法:
# ./docker_deplay.sh # 完整构建和部署流程 (默认AMD64架构)
# ./docker_deplay.sh -amd # 构建和部署AMD64架构
# ./docker_deplay.sh -arm # 构建和部署ARM64架构
# ./docker_deplay.sh -upload # 仅上传已存在的tar文件并部署
# ./docker_deplay.sh -upload -amd # 仅上传已存在的AMD64架构tar文件并部署
# ./docker_deplay.sh -upload -arm # 仅上传已存在的ARM64架构tar文件并部署
# =============================================================================
# 配置变量 - 请根据实际情况修改
SERVER_HOST="6.6.6.66" # 服务器IP地址
SERVER_USER="ubuntu" # 服务器用户名
SERVER_PASSWORD="qweasdzxc1" # 服务器密码
SERVER_PORT="22" # SSH端口默认22
IMAGE_NAME="epage_server" # Docker镜像名称
IMAGE_TAG="latest" # Docker镜像标签
CONTAINER_NAME="epage_server-container" # 容器名称
LOCAL_PORT="8199" # 本地端口
CONTAINER_PORT="8199" # 容器端口
TAR_FILE="${IMAGE_NAME}-${IMAGE_TAG}.tar" # 压缩包文件名
# 架构相关变量
PLATFORM="linux/amd64" # 默认架构
ARCH_SUFFIX="" # 架构后缀用于区分不同架构的tar文件
# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# 日志函数
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# 检查依赖
check_dependencies() {
log_info "检查依赖..."
if ! command -v docker &> /dev/null; then
log_error "Docker 未安装,请先安装 Docker"
exit 1
fi
if ! command -v sshpass &> /dev/null; then
log_error "sshpass 未安装,请先安装 sshpass"
log_info "macOS: brew install sshpass"
log_info "Ubuntu: sudo apt-get install sshpass"
exit 1
fi
log_success "依赖检查完成"
}
# 解析命令行参数
parse_arguments() {
while [[ $# -gt 0 ]]; do
case $1 in
-amd)
PLATFORM="linux/amd64"
ARCH_SUFFIX="-amd64"
log_info "设置目标架构为 AMD64"
shift
;;
-arm)
PLATFORM="linux/arm64"
ARCH_SUFFIX="-arm64"
log_info "设置目标架构为 ARM64"
shift
;;
-upload)
UPLOAD_ONLY=true
log_info "设置为仅上传模式"
shift
;;
*)
log_error "未知参数: $1"
log_info "支持的参数: -amd, -arm, -upload"
exit 1
;;
esac
done
# 更新TAR_FILE名包含架构后缀
TAR_FILE="${IMAGE_NAME}-${IMAGE_TAG}${ARCH_SUFFIX}.tar"
log_info "镜像文件名: ${TAR_FILE}"
}
# 构建Docker镜像
build_image() {
log_info "开始构建 Docker 镜像..."
# 检查是否存在旧的tar文件
if [ -f "$TAR_FILE" ]; then
log_warning "发现旧的tar文件正在删除..."
rm -f "$TAR_FILE"
fi
# 构建镜像并导出为tar文件
docker buildx build --platform $PLATFORM -t "${IMAGE_NAME}:${IMAGE_TAG}" --output type=docker,dest="./${TAR_FILE}" .
if [ $? -eq 0 ]; then
log_success "Docker 镜像构建完成: ${TAR_FILE}"
else
log_error "Docker 镜像构建失败"
exit 1
fi
}
# 上传文件到服务器
upload_to_server() {
log_info "上传文件到服务器..."
sshpass -p "$SERVER_PASSWORD" scp -P "$SERVER_PORT" -o StrictHostKeyChecking=no "$TAR_FILE" "${SERVER_USER}@${SERVER_HOST}:/tmp/"
if [ $? -eq 0 ]; then
log_success "文件上传成功"
else
log_error "文件上传失败"
exit 1
fi
}
# 在服务器上部署
deploy_on_server() {
log_info "在服务器上部署..."
sshpass -p "$SERVER_PASSWORD" ssh -p "$SERVER_PORT" -o StrictHostKeyChecking=no "${SERVER_USER}@${SERVER_HOST}" << EOF
set -e
echo "[INFO] 开始服务器端部署..."
# 检查并停止现有容器
if sudo docker ps -a --format 'table {{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
echo "[INFO] 发现现有容器 ${CONTAINER_NAME},正在停止并删除..."
sudo docker stop ${CONTAINER_NAME} || true
sudo docker rm ${CONTAINER_NAME} || true
fi
# 检查并删除现有镜像
if sudo docker images --format 'table {{.Repository}}:{{.Tag}}' | grep -q "^${IMAGE_NAME}:${IMAGE_TAG}$"; then
echo "[INFO] 发现现有镜像 ${IMAGE_NAME}:${IMAGE_TAG},正在删除..."
sudo docker rmi ${IMAGE_NAME}:${IMAGE_TAG} || true
fi
# 加载新镜像
echo "[INFO] 加载新镜像..."
sudo docker load -i /tmp/${TAR_FILE}
# 验证镜像是否加载成功
if sudo docker images | grep -q "${IMAGE_NAME}"; then
echo "[SUCCESS] 镜像加载成功"
else
echo "[ERROR] 镜像加载失败"
exit 1
fi
# 运行新容器
echo "[INFO] 启动新容器..."
sudo docker run -d -p ${LOCAL_PORT}:${CONTAINER_PORT} --name ${CONTAINER_NAME} ${IMAGE_NAME}:${IMAGE_TAG}
# 验证容器是否启动成功
if sudo docker ps | grep -q "${CONTAINER_NAME}"; then
echo "[SUCCESS] 容器启动成功"
echo "[INFO] 容器状态:"
sudo docker ps | grep "${CONTAINER_NAME}"
else
echo "[ERROR] 容器启动失败"
echo "[INFO] 查看容器日志:"
sudo docker logs ${CONTAINER_NAME}
exit 1
fi
# 清理临时文件
echo "[INFO] 清理临时文件..."
rm -f /tmp/${TAR_FILE}
echo "[SUCCESS] 部署完成!"
echo "[INFO] 应用访问地址: http://${SERVER_HOST}:${LOCAL_PORT}"
EOF
if [ $? -eq 0 ]; then
log_success "服务器部署完成"
else
log_error "服务器部署失败"
exit 1
fi
}
# 清理本地文件
cleanup_local() {
log_info "清理本地临时文件..."
if [ -f "$TAR_FILE" ]; then
rm -f "$TAR_FILE"
log_success "本地临时文件已清理"
fi
}
# 显示部署信息
show_deployment_info() {
log_success "部署完成!"
echo ""
echo "=========================================="
echo "部署信息:"
echo "=========================================="
echo "服务器地址: ${SERVER_HOST}"
echo "应用端口: ${LOCAL_PORT}"
echo "访问地址: http://${SERVER_HOST}:${LOCAL_PORT}"
echo "容器名称: ${CONTAINER_NAME}"
echo "镜像名称: ${IMAGE_NAME}:${IMAGE_TAG}"
echo "目标架构: ${PLATFORM}"
echo "镜像文件: ${TAR_FILE}"
echo "=========================================="
echo ""
log_info "如需查看容器日志,请在服务器上运行: sudo docker logs ${CONTAINER_NAME}"
log_info "如需停止容器,请在服务器上运行: sudo docker stop ${CONTAINER_NAME}"
}
# 主函数
main() {
echo "=========================================="
echo "Docker 镜像构建和部署自动化脚本"
echo "=========================================="
echo ""
# 解析命令行参数
UPLOAD_ONLY=false
parse_arguments "$@"
# 检查配置
if [ "$SERVER_HOST" = "your-server-ip" ] || [ "$SERVER_PASSWORD" = "your-password" ]; then
log_error "请先配置脚本顶部的服务器信息"
log_info "需要修改的变量:"
log_info " - SERVER_HOST: 服务器IP地址"
log_info " - SERVER_USER: 服务器用户名"
log_info " - SERVER_PASSWORD: 服务器密码"
exit 1
fi
# 检查是否是上传模式
if [ "$UPLOAD_ONLY" = true ]; then
log_info "检测到 -upload 参数,跳过构建步骤"
# 检查tar文件是否存在
if [ ! -f "$TAR_FILE" ]; then
log_error "未找到tar文件: $TAR_FILE"
log_info "请先运行脚本构建镜像或确保tar文件存在"
exit 1
fi
log_success "找到tar文件: $TAR_FILE"
# 执行上传和部署流程
upload_to_server
deploy_on_server
cleanup_local
show_deployment_info
else
# 执行完整的部署流程
check_dependencies
build_image
upload_to_server
deploy_on_server
cleanup_local
show_deployment_info
fi
}
# 脚本入口
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi

0
luna_20260302_1211.dump Normal file
View File

30
main.py
View File

@@ -1,6 +1,7 @@
from fastapi import FastAPI, Request
from fastapi.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware
from starlette.middleware.sessions import SessionMiddleware
from contextlib import asynccontextmanager
import logging
import os
@@ -10,6 +11,7 @@ from database import init_db
from mqtt_manager import mqtt_manager
from api import api_router
from admin_routes import admin_router
from auth import APIKeyMiddleware, AdminAuthMiddleware
# 配置日志
logging.basicConfig(
@@ -51,10 +53,21 @@ async def lifespan(app: FastAPI):
# 创建FastAPI应用
app = FastAPI(
title=settings.app_name,
description="基于 FastAPI + MQTT + HTTP/HTTPS + NTP 的轻量级墨水屏显示系统服务端",
title="墨水屏桌面屏幕系统 API",
description="用于管理墨水屏设备、内容和待办事项的API",
version="1.0.0",
lifespan=lifespan
lifespan=lifespan,
openapi_components={
"securitySchemes": {
"APIKeyHeader": {
"type": "apiKey",
"in": "header",
"name": "X-API-Key",
"description": "API Key鉴权请在下方输入正确的API Key"
}
}
},
security=[{"APIKeyHeader": []}]
)
# 添加CORS中间件
@@ -66,11 +79,20 @@ app.add_middleware(
allow_headers=["*"],
)
# 添加API Key鉴权中间件
app.add_middleware(APIKeyMiddleware)
# 添加Admin认证中间件
app.add_middleware(AdminAuthMiddleware)
# 添加Session中间件
app.add_middleware(SessionMiddleware, secret_key=settings.secret_key)
# 挂载静态文件
app.mount("/static", StaticFiles(directory=settings.static_dir), name="static")
# 注册API路由
app.include_router(api_router)
app.include_router(api_router, prefix="/api")
# 包含管理后台路由
app.include_router(admin_router, prefix="/admin", tags=["管理后台"])

View File

@@ -2,6 +2,6 @@ from sqlalchemy.orm import Session
from database import Base
# 导入所有模型以确保它们被注册到Base.metadata
from database import Device, Content
from database import Device, Content, Todo
__all__ = ["Device", "Content"]
__all__ = ["Device", "Content", "Todo"]

10
mosquitto.conf Normal file
View File

@@ -0,0 +1,10 @@
# Mosquitto配置文件
# 监听端口
listener 1883
# 允许匿名连接默认为true我们将设置为false以要求认证
allow_anonymous false
# 密码文件
password_file /mosquitto/config/passwd

View File

@@ -166,5 +166,42 @@ class MQTTManager:
except Exception as e:
logger.error(f"取消订阅设备状态失败: {str(e)}")
def send_todo_command(self, device_id: str, action: str, todo_data: Dict[str, Any]) -> bool:
"""
发送待办事项命令
Args:
device_id: 设备ID
action: 动作类型 (create, update, delete)
todo_data: 待办事项数据
Returns:
是否发送成功
"""
if not self.connected:
logger.error("MQTT未连接无法发送待办事项命令")
return False
try:
topic = f"esp32/{device_id}/todo"
payload = {
"type": "todo",
"action": action,
"data": todo_data,
"timestamp": int(time.time())
}
result = self.client.publish(topic, json.dumps(payload))
if result.rc == mqtt.MQTT_ERR_SUCCESS:
logger.info(f"成功向设备 {device_id} 发送待办事项命令: {action}")
return True
else:
logger.error(f"向设备 {device_id} 发送待办事项命令失败,错误码: {result.rc}")
return False
except Exception as e:
logger.error(f"发送待办事项命令失败: {str(e)}")
return False
# 全局MQTT管理器实例
mqtt_manager = MQTTManager()

View File

@@ -11,3 +11,5 @@ python-jose[cryptography]==3.3.0
passlib[bcrypt]==1.7.4
aiofiles==23.2.1
httpx==0.25.2
itsdangerous==2.1.2
jinja2==3.1.2

View File

@@ -87,3 +87,29 @@ class MQTTStatus(BaseModel):
timestamp: int = Field(..., description="时间戳")
device_id: str = Field(..., description="设备ID")
message: Optional[str] = None
# 待办事项相关模型
class TodoBase(BaseModel):
title: str = Field(..., description="待办事项标题")
description: Optional[str] = Field(None, description="待办事项描述")
due_date: Optional[datetime] = Field(None, description="截止日期")
class TodoCreate(TodoBase):
device_id: str = Field(..., description="设备ID")
class TodoUpdate(BaseModel):
title: Optional[str] = None
description: Optional[str] = None
due_date: Optional[datetime] = None
is_completed: Optional[bool] = None
class Todo(TodoBase):
id: int
device_id: str
is_completed: bool
completed_at: Optional[datetime] = None
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True

11
setup_mqtt_auth.sh Executable file
View File

@@ -0,0 +1,11 @@
#!/bin/bash
# 创建MQTT密码文件
docker exec luna-mqtt mosquitto_passwd -c -b /mosquitto/config/passwd luna2025 123luna2021
# 重启MQTT服务以应用新的认证配置
docker restart luna-mqtt
echo "MQTT用户名和密码已配置完成"
echo "用户名: luna2025"
echo "密码: 123luna2021"

BIN
static/.DS_Store vendored Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 457 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

@@ -35,6 +35,11 @@
<i class="fas fa-file-image me-1"></i>内容管理
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if '/admin/todos' in request.url.path %}active{% endif %}" href="/admin/todos">
<i class="fas fa-tasks me-1"></i>待办事项
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if '/admin/upload' in request.url.path %}active{% endif %}" href="/admin/upload">
<i class="fas fa-upload me-1"></i>图片上传
@@ -45,7 +50,22 @@
<button class="btn btn-outline-light me-2" id="themeToggle" title="切换主题">
<i class="fas fa-palette"></i>
</button>
<span class="navbar-text text-light">
<div class="dropdown">
<button class="btn btn-outline-light dropdown-toggle" type="button" id="userDropdown" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fas fa-user-circle me-1"></i>
{% if request.session.get('username') %}
{{ request.session.get('username') }}
{% else %}
管理员
{% endif %}
</button>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="userDropdown">
<li><a class="dropdown-item" href="/admin/logout">
<i class="fas fa-sign-out-alt me-1"></i>登出
</a></li>
</ul>
</div>
<span class="navbar-text text-light ms-2">
<i class="far fa-clock me-1"></i>
<span id="currentTime"></span>
</span>

View File

@@ -100,20 +100,45 @@
<div class="row">
<div class="col-5">
<div class="icon-big text-center">
<i class="fas fa-play-circle"></i>
<i class="fas fa-tasks"></i>
</div>
</div>
<div class="col-7">
<div class="numbers">
<p class="card-category">活跃内容</p>
<h3 class="card-title">{{ active_content_count }}</h3>
<p class="card-category">待办事项</p>
<h3 class="card-title">{{ todo_count }}</h3>
</div>
</div>
</div>
</div>
<div class="card-footer">
<div class="stats">
<i class="fas fa-chart-line"></i> 活跃率: {{ ((active_content_count / content_count * 100) | round(1) if content_count > 0 else 0) }}%
<i class="fas fa-check-circle"></i> 完成率: {{ ((completed_todo_count / todo_count * 100) | round(1) if todo_count > 0 else 0) }}%
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-4">
<div class="card card-stats bg-danger text-white shadow-sm">
<div class="card-body">
<div class="row">
<div class="col-5">
<div class="icon-big text-center">
<i class="fas fa-exclamation-triangle"></i>
</div>
</div>
<div class="col-7">
<div class="numbers">
<p class="card-category">待完成</p>
<h3 class="card-title">{{ pending_todo_count }}</h3>
</div>
</div>
</div>
</div>
<div class="card-footer">
<div class="stats">
<i class="fas fa-clock"></i> 待处理任务
</div>
</div>
</div>
@@ -172,36 +197,44 @@
</div>
</div>
<!-- 最近创建的内容 -->
<!-- 最近待办事项 -->
<div class="col-lg-6 mb-4">
<div class="card shadow-sm">
<div class="card-header bg-white py-3 d-flex flex-row align-items-center justify-content-between">
<h6 class="m-0 font-weight-bold text-primary">
<i class="fas fa-clock me-2"></i>最近创建的内容
<i class="fas fa-tasks me-2"></i>最近待办事项
</h6>
<a href="/admin/contents" class="btn btn-sm btn-primary">
<a href="/admin/todos" class="btn btn-sm btn-primary">
<i class="fas fa-list me-1"></i>查看全部
</a>
</div>
<div class="card-body">
{% if recent_contents %}
{% if recent_todos %}
<div class="table-responsive">
<table class="table table-hover">
<thead class="table-light">
<tr>
<th><i class="fas fa-barcode me-1"></i>设备ID</th>
<th><i class="fas fa-heading me-1"></i>标题</th>
<th><i class="fas fa-code-branch me-1"></i>版本</th>
<th><i class="fas fa-mobile-alt me-1"></i>设备</th>
<th><i class="fas fa-check me-1"></i>状态</th>
<th><i class="fas fa-calendar me-1"></i>创建时间</th>
</tr>
</thead>
<tbody>
{% for content in recent_contents %}
{% for item in recent_todos %}
{% set todo = item.todo %}
{% set device = item.device %}
<tr>
<td><a href="/admin/devices/{{ content.device_id }}" class="text-decoration-none"><code>{{ content.device_id }}</code></a></td>
<td><a href="/admin/devices/{{ content.device_id }}/contents/{{ content.version }}" class="text-decoration-none">{{ content.title }}</a></td>
<td><span class="badge bg-info">v{{ content.version }}</span></td>
<td>{{ content.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
<td><a href="/admin/todos/{{ todo.id }}" class="text-decoration-none">{{ todo.title }}</a></td>
<td><a href="/admin/devices/{{ device.device_id }}" class="text-decoration-none">{{ device.name or device.device_id }}</a></td>
<td>
{% if todo.is_completed %}
<span class="badge bg-success"><i class="fas fa-check me-1"></i>已完成</span>
{% else %}
<span class="badge bg-warning"><i class="fas fa-clock me-1"></i>待完成</span>
{% endif %}
</td>
<td>{{ todo.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
</tr>
{% endfor %}
</tbody>
@@ -209,8 +242,8 @@
</div>
{% else %}
<div class="text-center py-4">
<i class="fas fa-inbox fa-3x text-muted mb-3"></i>
<p class="text-muted">暂无内容</p>
<i class="fas fa-clipboard-list fa-3x text-muted mb-3"></i>
<p class="text-muted">暂无待办事项</p>
</div>
{% endif %}
</div>
@@ -240,13 +273,13 @@
</a>
</div>
<div class="col-md-3 mb-3">
<a href="/admin/upload" class="btn btn-success btn-lg btn-block">
<i class="fas fa-upload me-2"></i> 上传图片
<a href="/admin/todos/add" class="btn btn-warning btn-lg btn-block">
<i class="fas fa-plus me-2"></i> 添加待办
</a>
</div>
<div class="col-md-3 mb-3">
<a href="/admin/devices" class="btn btn-warning btn-lg btn-block">
<i class="fas fa-tv me-2"></i> 设备管理
<a href="/admin/todos" class="btn btn-success btn-lg btn-block">
<i class="fas fa-tasks me-2"></i> 待办管理
</a>
</div>
</div>

View File

@@ -0,0 +1,74 @@
{% extends "admin/base.html" %}
{% block title %}管理员登录{% endblock %}
{% block content %}
<div class="login-container">
<div class="login-card">
<div class="login-header">
<h2>管理员登录</h2>
<p class="text-muted">墨水屏桌面屏幕系统</p>
</div>
{% if error %}
<div class="alert alert-danger" role="alert">
{{ error }}
</div>
{% endif %}
<form method="post" action="/admin/login">
<div class="mb-3">
<label for="username" class="form-label">用户名</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">密码</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="remember" name="remember">
<label class="form-check-label" for="remember">记住我</label>
</div>
<button type="submit" class="btn btn-primary w-100">登录</button>
</form>
</div>
</div>
<style>
.login-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background-color: #f8f9fa;
}
.login-card {
background: white;
border-radius: 10px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
padding: 2rem;
width: 100%;
max-width: 400px;
}
.login-header {
text-align: center;
margin-bottom: 2rem;
}
.login-header h2 {
margin-bottom: 0.5rem;
color: #333;
}
.login-header p {
margin-bottom: 0;
color: #6c757d;
font-size: 0.9rem;
}
</style>
{% endblock %}

View File

@@ -0,0 +1,108 @@
{% extends "admin/base.html" %}
{% block title %}添加待办事项{% endblock %}
{% block content %}
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">
<i class="fas fa-plus me-2"></i>添加待办事项
</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<div class="btn-group me-2">
<a href="/admin/todos{% if device_id %}?device_id={{ device_id }}{% endif %}" class="btn btn-sm btn-outline-secondary">
<i class="fas fa-arrow-left me-1"></i>返回列表
</a>
</div>
</div>
</div>
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/admin">首页</a></li>
<li class="breadcrumb-item"><a href="/admin/todos{% if device_id %}?device_id={{ device_id }}{% endif %}">待办事项管理</a></li>
<li class="breadcrumb-item active">添加待办事项</li>
</ol>
</nav>
<div class="row">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h5 class="mb-0">
<i class="fas fa-clipboard-list me-2"></i>待办事项信息
</h5>
</div>
<div class="card-body">
<form method="post" action="/admin/todos/add">
<div class="mb-3">
<label for="title" class="form-label">标题 <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="title" name="title" required>
</div>
<div class="mb-3">
<label for="description" class="form-label">描述</label>
<textarea class="form-control" id="description" name="description" rows="3"></textarea>
</div>
<div class="mb-3">
<label for="device_id" class="form-label">关联设备 <span class="text-danger">*</span></label>
<select class="form-select" id="device_id" name="device_id" required>
<option value="">请选择设备</option>
{% for device in devices %}
<option value="{{ device.device_id }}" {% if device_id and device.device_id == device_id %}selected{% endif %}>
{{ device.name or device.device_id }}
</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label for="due_date" class="form-label">截止时间</label>
<input type="datetime-local" class="form-control" id="due_date" name="due_date">
<div class="form-text">留空表示无截止时间</div>
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="is_completed" name="is_completed">
<label class="form-check-label" for="is_completed">
标记为已完成
</label>
</div>
</div>
<div class="d-flex justify-content-between">
<a href="/admin/todos{% if device_id %}?device_id={{ device_id }}{% endif %}" class="btn btn-secondary">
<i class="fas fa-times me-1"></i>取消
</a>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save me-1"></i>保存
</button>
</div>
</form>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
<h5 class="mb-0">
<i class="fas fa-info-circle me-2"></i>帮助信息
</h5>
</div>
<div class="card-body">
<p>创建一个新的待办事项:</p>
<ul>
<li><strong>标题:</strong>待办事项的简短描述</li>
<li><strong>描述:</strong>待办事项的详细说明(可选)</li>
<li><strong>关联设备:</strong>选择要显示此待办事项的设备</li>
<li><strong>截止时间:</strong>设置待办事项的截止时间(可选)</li>
<li><strong>状态:</strong>可以选择直接标记为已完成</li>
</ul>
<p class="text-muted">创建后,待办事项将发送到关联的设备,并可以在设备上标记完成状态。</p>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,161 @@
{% extends "admin/base.html" %}
{% block title %}待办事项详情{% endblock %}
{% block content %}
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">
<i class="fas fa-tasks me-2"></i>待办事项详情
</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<div class="btn-group me-2">
<a href="/admin/todos{% if todo.device_id %}?device_id={{ todo.device_id }}{% endif %}" class="btn btn-sm btn-outline-secondary">
<i class="fas fa-arrow-left me-1"></i>返回列表
</a>
</div>
</div>
</div>
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/admin">首页</a></li>
<li class="breadcrumb-item"><a href="/admin/todos{% if todo.device_id %}?device_id={{ todo.device_id }}{% endif %}">待办事项管理</a></li>
<li class="breadcrumb-item active">{{ todo.title }}</li>
</ol>
</nav>
<div class="row">
<div class="col-md-8">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">
<i class="fas fa-clipboard-list me-2"></i>待办事项信息
</h5>
<span class="badge {% if todo.is_completed %}bg-success{% else %}bg-warning{% %} fs-6">
{% if todo.is_completed %}已完成{% else %}未完成{% endif %}
</span>
</div>
<div class="card-body">
<div class="row mb-3">
<div class="col-sm-3 fw-bold">标题:</div>
<div class="col-sm-9">{{ todo.title }}</div>
</div>
<div class="row mb-3">
<div class="col-sm-3 fw-bold">描述:</div>
<div class="col-sm-9">{{ todo.description or '无' }}</div>
</div>
<div class="row mb-3">
<div class="col-sm-3 fw-bold">关联设备:</div>
<div class="col-sm-9">
<a href="/admin/devices/{{ device.device_id }}" class="text-decoration-none">
{{ device.name or device.device_id }}
</a>
</div>
</div>
<div class="row mb-3">
<div class="col-sm-3 fw-bold">截止时间:</div>
<div class="col-sm-9">
{% if todo.due_date %}
{{ todo.due_date.strftime('%Y-%m-%d %H:%M') }}
{% else %}
{% endif %}
</div>
</div>
<div class="row mb-3">
<div class="col-sm-3 fw-bold">创建时间:</div>
<div class="col-sm-9">{{ todo.created_at.strftime('%Y-%m-%d %H:%M:%S') }}</div>
</div>
<div class="row mb-3">
<div class="col-sm-3 fw-bold">更新时间:</div>
<div class="col-sm-9">{{ todo.updated_at.strftime('%Y-%m-%d %H:%M:%S') }}</div>
</div>
{% if todo.is_completed and todo.completed_at %}
<div class="row mb-3">
<div class="col-sm-3 fw-bold">完成时间:</div>
<div class="col-sm-9">{{ todo.completed_at.strftime('%Y-%m-%d %H:%M:%S') }}</div>
</div>
{% endif %}
</div>
<div class="card-footer">
<div class="btn-group">
<a href="/admin/todos/{{ todo.id }}/edit" class="btn btn-primary">
<i class="fas fa-edit me-1"></i>编辑
</a>
<form method="post" action="/admin/todos/{{ todo.id }}/toggle" style="display: inline;">
<button type="submit" class="btn {% if todo.is_completed %}btn-warning{% else %}btn-success{% %}">
<i class="fas {% if todo.is_completed %}fa-undo{% else %}fa-check{% %} me-1"></i>
{% if todo.is_completed %}标记为未完成{% else %}标记为已完成{% endif %}
</button>
</form>
<form method="post" action="/admin/todos/{{ todo.id }}/delete" style="display: inline;" onsubmit="return confirm('确定要删除这个待办事项吗?');">
<button type="submit" class="btn btn-danger">
<i class="fas fa-trash me-1"></i>删除
</button>
</form>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
<h5 class="mb-0">
<i class="fas fa-info-circle me-2"></i>设备信息
</h5>
</div>
<div class="card-body">
<div class="row mb-3">
<div class="col-sm-4 fw-bold">设备ID:</div>
<div class="col-sm-8">{{ device.device_id }}</div>
</div>
<div class="row mb-3">
<div class="col-sm-4 fw-bold">设备名称:</div>
<div class="col-sm-8">{{ device.name or '未设置' }}</div>
</div>
<div class="row mb-3">
<div class="col-sm-4 fw-bold">设备类型:</div>
<div class="col-sm-8">{{ device.device_type or '未知' }}</div>
</div>
<div class="row mb-3">
<div class="col-sm-4 fw-bold">状态:</div>
<div class="col-sm-8">
{% if device.status == 'online' %}
<span class="badge bg-success">在线</span>
{% else %}
<span class="badge bg-danger">离线</span>
{% endif %}
</div>
</div>
<div class="row mb-3">
<div class="col-sm-4 fw-bold">最后活跃:</div>
<div class="col-sm-8">
{% if device.last_seen %}
{{ device.last_seen.strftime('%Y-%m-%d %H:%M:%S') }}
{% else %}
未知
{% endif %}
</div>
</div>
<div class="text-center mt-3">
<a href="/admin/devices/{{ device.device_id }}" class="btn btn-outline-primary btn-sm">
<i class="fas fa-tv me-1"></i>查看设备详情
</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,121 @@
{% extends "admin/base.html" %}
{% block title %}编辑待办事项{% endblock %}
{% block content %}
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">
<i class="fas fa-edit me-2"></i>编辑待办事项
</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<div class="btn-group me-2">
<a href="/admin/todos/{{ todo.id }}" class="btn btn-sm btn-outline-secondary">
<i class="fas fa-arrow-left me-1"></i>返回详情
</a>
</div>
</div>
</div>
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/admin">首页</a></li>
<li class="breadcrumb-item"><a href="/admin/todos{% if todo.device_id %}?device_id={{ todo.device_id }}{% endif %}">待办事项管理</a></li>
<li class="breadcrumb-item"><a href="/admin/todos/{{ todo.id }}">{{ todo.title }}</a></li>
<li class="breadcrumb-item active">编辑</li>
</ol>
</nav>
<div class="row">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h5 class="mb-0">
<i class="fas fa-clipboard-list me-2"></i>待办事项信息
</h5>
</div>
<div class="card-body">
<form method="post" action="/admin/todos/{{ todo.id }}/edit">
<div class="mb-3">
<label for="title" class="form-label">标题 <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="title" name="title" value="{{ todo.title }}" required>
</div>
<div class="mb-3">
<label for="description" class="form-label">描述</label>
<textarea class="form-control" id="description" name="description" rows="3">{{ todo.description or '' }}</textarea>
</div>
<div class="mb-3">
<label for="device_id" class="form-label">关联设备 <span class="text-danger">*</span></label>
<select class="form-select" id="device_id" name="device_id" required>
{% for device in devices %}
<option value="{{ device.device_id }}" {% if device.device_id == todo.device_id %}selected{% endif %}>
{{ device.name or device.device_id }}
</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label for="due_date" class="form-label">截止时间</label>
<input type="datetime-local" class="form-control" id="due_date" name="due_date"
{% if todo.due_date %}value="{{ todo.due_date.strftime('%Y-%m-%dT%H:%M') }}"{% endif %}>
<div class="form-text">留空表示无截止时间</div>
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="is_completed" name="is_completed"
{% if todo.is_completed %}checked{% endif %}>
<label class="form-check-label" for="is_completed">
标记为已完成
</label>
</div>
</div>
<div class="d-flex justify-content-between">
<a href="/admin/todos/{{ todo.id }}" class="btn btn-secondary">
<i class="fas fa-times me-1"></i>取消
</a>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save me-1"></i>保存更改
</button>
</div>
</form>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
<h5 class="mb-0">
<i class="fas fa-info-circle me-2"></i>当前信息
</h5>
</div>
<div class="card-body">
<div class="row mb-2">
<div class="col-sm-4 fw-bold">创建时间:</div>
<div class="col-sm-8">{{ todo.created_at.strftime('%Y-%m-%d %H:%M') }}</div>
</div>
<div class="row mb-2">
<div class="col-sm-4 fw-bold">更新时间:</div>
<div class="col-sm-8">{{ todo.updated_at.strftime('%Y-%m-%d %H:%M') }}</div>
</div>
{% if todo.is_completed and todo.completed_at %}
<div class="row mb-2">
<div class="col-sm-4 fw-bold">完成时间:</div>
<div class="col-sm-8">{{ todo.completed_at.strftime('%Y-%m-%d %H:%M') }}</div>
</div>
{% endif %}
<hr>
<p class="text-muted">修改待办事项后,更新将发送到关联的设备。</p>
</div>
</div>
</div>
</div>
{% endblock %}

218
templates/admin/todos.html Normal file
View File

@@ -0,0 +1,218 @@
{% extends "admin/base.html" %}
{% block title %}待办事项管理{% endblock %}
{% block content %}
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">
<i class="fas fa-tasks me-2"></i>待办事项管理
{% if filtered %}
<small class="text-muted">- {{ device.name or device.device_id }}</small>
{% endif %}
</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<div class="btn-group me-2">
<a href="/admin/todos/add{% if filtered %}?device_id={{ device.device_id }}{% endif %}" class="btn btn-sm btn-outline-secondary">
<i class="fas fa-plus me-1"></i>添加待办事项
</a>
</div>
{% if not filtered %}
<div class="btn-group">
<button type="button" class="btn btn-sm btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fas fa-filter me-1"></i>筛选设备
</button>
<ul class="dropdown-menu">
{% for device in devices %}
<li><a class="dropdown-item" href="/admin/todos?device_id={{ device.device_id }}">{{ device.name or device.device_id }}</a></li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
</div>
{% if filtered %}
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/admin/todos">所有待办事项</a></li>
<li class="breadcrumb-item active">{{ device.name or device.device_id }}</li>
</ol>
</nav>
{% endif %}
<div class="row">
<div class="col-12">
{% if filtered %}
{% if todos %}
<div class="card">
<div class="card-header">
<i class="fas fa-list me-2"></i>待办事项列表
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>标题</th>
<th>描述</th>
<th>截止时间</th>
<th>状态</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for todo in todos %}
<tr>
<td>
<a href="/admin/todos/{{ todo.id }}" class="text-decoration-none">
{{ todo.title }}
</a>
</td>
<td>{{ todo.description or '-' }}</td>
<td>
{% if todo.due_date %}
{{ todo.due_date.strftime('%Y-%m-%d %H:%M') }}
{% else %}
-
{% endif %}
</td>
<td>
{% if todo.is_completed %}
<span class="badge bg-success">已完成</span>
{% if todo.completed_at %}
<small class="text-muted d-block">{{ todo.completed_at.strftime('%Y-%m-%d %H:%M') }}</small>
{% endif %}
{% else %}
<span class="badge bg-warning">未完成</span>
{% endif %}
</td>
<td>{{ todo.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
<td>
<div class="btn-group btn-group-sm" role="group">
<a href="/admin/todos/{{ todo.id }}" class="btn btn-outline-primary" title="查看详情">
<i class="fas fa-eye"></i>
</a>
<form method="post" action="/admin/todos/{{ todo.id }}/toggle" style="display: inline;">
<button type="submit" class="btn {% if todo.is_completed %}btn-outline-warning{% else %}btn-outline-success{% endif %}" title="{% if todo.is_completed %}标记为未完成{% else %}标记为已完成{% endif %}">
<i class="fas {% if todo.is_completed %}fa-undo{% else %}fa-check{% endif %}"></i>
</button>
</form>
<form method="post" action="/admin/todos/{{ todo.id }}/delete" style="display: inline;" onsubmit="return confirm('确定要删除这个待办事项吗?');">
<button type="submit" class="btn btn-outline-danger" title="删除">
<i class="fas fa-trash"></i>
</button>
</form>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% else %}
<div class="card">
<div class="card-body text-center py-5">
<i class="fas fa-clipboard-list fa-3x text-muted mb-3"></i>
<h5 class="text-muted">该设备暂无待办事项</h5>
<p class="text-muted">点击上方按钮添加第一个待办事项</p>
<a href="/admin/todos/add?device_id={{ device.device_id }}" class="btn btn-primary">
<i class="fas fa-plus me-1"></i>添加待办事项
</a>
</div>
</div>
{% endif %}
{% else %}
{% if todo_list %}
<div class="card">
<div class="card-header">
<i class="fas fa-list me-2"></i>所有待办事项
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>标题</th>
<th>设备</th>
<th>描述</th>
<th>截止时间</th>
<th>状态</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for item in todo_list %}
<tr>
<td>
<a href="/admin/todos/{{ item.todo.id }}" class="text-decoration-none">
{{ item.todo.title }}
</a>
</td>
<td>
<a href="/admin/devices/{{ item.device.device_id }}" class="text-decoration-none">
{{ item.device.name or item.device.device_id }}
</a>
</td>
<td>{{ item.todo.description or '-' }}</td>
<td>
{% if item.todo.due_date %}
{{ item.todo.due_date.strftime('%Y-%m-%d %H:%M') }}
{% else %}
-
{% endif %}
</td>
<td>
{% if item.todo.is_completed %}
<span class="badge bg-success">已完成</span>
{% if item.todo.completed_at %}
<small class="text-muted d-block">{{ item.todo.completed_at.strftime('%Y-%m-%d %H:%M') }}</small>
{% endif %}
{% else %}
<span class="badge bg-warning">未完成</span>
{% endif %}
</td>
<td>{{ item.todo.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
<td>
<div class="btn-group btn-group-sm" role="group">
<a href="/admin/todos/{{ item.todo.id }}" class="btn btn-outline-primary" title="查看详情">
<i class="fas fa-eye"></i>
</a>
<form method="post" action="/admin/todos/{{ item.todo.id }}/toggle" style="display: inline;">
<button type="submit" class="btn {% if item.todo.is_completed %}btn-outline-warning{% else %}btn-outline-success{% endif %}" title="{% if item.todo.is_completed %}标记为未完成{% else %}标记为已完成{% endif %}">
<i class="fas {% if item.todo.is_completed %}fa-undo{% else %}fa-check{% endif %}"></i>
</button>
</form>
<form method="post" action="/admin/todos/{{ item.todo.id }}/delete" style="display: inline;" onsubmit="return confirm('确定要删除这个待办事项吗?');">
<button type="submit" class="btn btn-outline-danger" title="删除">
<i class="fas fa-trash"></i>
</button>
</form>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% else %}
<div class="card">
<div class="card-body text-center py-5">
<i class="fas fa-clipboard-list fa-3x text-muted mb-3"></i>
<h5 class="text-muted">暂无待办事项</h5>
<p class="text-muted">点击上方按钮添加第一个待办事项</p>
<a href="/admin/todos/add" class="btn btn-primary">
<i class="fas fa-plus me-1"></i>添加待办事项
</a>
</div>
</div>
{% endif %}
{% endif %}
</div>
</div>
{% endblock %}

BIN
test_image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,167 @@
# 图片转换工具使用指南
这个工具可以将多种格式的图片转换为适用于ESP32墨水屏显示的二进制点阵数据。
## 功能特点
- 支持多种图片格式JPG、PNG、BMP、GIF、TIFF等
- 自动调整图片大小并居中显示
- 支持黑色背景白色文本或白色背景黑色文本
- 支持旋转90度显示
- 支持Floyd-Steinberg抖动算法提高图像质量
- 支持创建文本图像
- 支持批量转换
## 安装依赖
```bash
pip install Pillow
```
## 使用方法
### 1. 转换单个图片
```bash
python3 image_converter.py image <输入图片路径> <输出文件路径> [选项]
```
```bash
cd "/Users/jeremygan/Desktop/
TangledupAI/esp32_GDEY042T81-24Pin-_dirver-" && python3 tool/image_con
verter.py image tool/test_iamge_white_bg2.png images/test_image2.py --
width 400 --height 300
```
选项参数:
- `--width`: 目标宽度默认400
- `--height`: 目标高度默认300
- `--invert`: 反转颜色(白色背景黑色文本)
- `--rotate`: 旋转90度
- `--no-dither`: 不使用抖动算法
示例:
```bash
# 转换为黑色背景白色文本(默认)
python3 image_converter.py image photo.jpg photo_dark.py
# 转换为白色背景黑色文本
python3 image_converter.py image photo.jpg photo_light.py --invert
# 转换并旋转90度
python3 image_converter.py image photo.jpg photo_rotated.py --rotate
# 转换为128x296尺寸并旋转
python3 image_converter.py image photo.jpg photo_small.py --width 128 --height 296 --rotate
```
### 2. 创建文本图像
```bash
python3 image_converter.py text "<文本内容>" <输出文件路径> [选项]
```
选项参数:
- `--width`: 目标宽度默认400
- `--height`: 目标高度默认300
- `--font-size`: 字体大小默认24
- `--invert`: 反转颜色(白色背景黑色文本)
- `--rotate`: 旋转90度
示例:
```bash
# 创建文本图像
python3 image_converter.py text "Hello ESP32!" hello.py
# 创建大字体文本图像
python3 image_converter.py text "Hello ESP32!" hello_big.py --font-size 32
# 创建旋转文本图像
python3 image_converter.py text "Rotated Text" rotated.py --rotate
```
### 3. 批量转换
```bash
python3 image_converter.py batch <输入目录> <输出目录> [选项]
```
示例:
```bash
# 批量转换目录中的所有图片
python3 image_converter.py batch images/ converted/
# 批量转换为白色背景黑色文本
python3 image_converter.py batch images/ converted/ --invert
```
## 输出格式
转换后的文件包含一个bytearray变量变量名与文件名相同
```python
# Converted from photo.jpg
# Size: 400x300
# Inverted: False, Rotated: False
photo = bytearray(b'\x00\x00\x00\x0...')
```
## 在ESP32项目中使用
1. 将转换后的.py文件复制到ESP32项目目录
2. 在boot.py中设置`RUN_MODE=3`
3. 修改image.py导入转换后的图片数据
```python
# 在image.py中导入转换后的图片
from photo import photo
def run(img_data=None):
if img_data is None:
# 使用默认图片
img_data = photo
# ... 其余代码保持不变
```
4. 上传代码到ESP32并运行
## 示例
运行示例脚本:
```bash
python3 convert_examples.py
```
这将创建示例图片目录并展示各种转换用法。
## 注意事项
1. 图片会自动调整大小并居中显示,保持原始宽高比
2. 使用抖动算法可以提高图像质量,但会增加文件大小
3. 旋转90度后宽度和高度会互换
4. 确保输出目录有写入权限
5. 转换大图片可能需要一些时间
## 故障排除
1. **ImportError: No module named 'PIL'**
- 解决方案:运行 `pip install Pillow`
2. **无法识别的图片格式**
- 解决方案确保图片格式受支持JPG、PNG、BMP、GIF、TIFF
3. **转换后的图片显示不正确**
- 解决方案:尝试使用`--invert`参数反转颜色
- 尝试使用`--rotate`参数旋转图片
4. **内存不足**
- 解决方案:减小目标尺寸或使用更小的源图片
## 技术细节
- 图片使用Floyd-Steinberg抖动算法转换为1位黑白图像
- 每个字节表示8个像素最高位在前
- 像素值0表示黑色1表示白色
- 字节数组按行组织,每行需要的字节数为(宽度+7)//8

144
tool/USAGE_CN.md Normal file
View File

@@ -0,0 +1,144 @@
# 图片转换工具使用说明
## 问题修复说明
### 之前的问题
转换器在生成的图片数据中错误地插入了换行符 (`\n`, 0x0A),导致墨水屏显示乱码。
### 修复内容
修改了 `image_converter.py` 中的字节数组格式化逻辑:
- **之前**: 每16个字节后直接写入 `\n`,导致换行符成为数据的一部分
- **现在**: 每16个字节后正确地分割字符串为多个 `b'...'` 部分
## 使用方法
### 1. 转换单个图片
```bash
python image_converter.py image <输入图片路径> <输出文件路径> [选项]
```
**示例:**
```bash
# 基本用法
python image_converter.py image test_image.png output.py
# 指定尺寸
python image_converter.py image test_image.png output.py --width 400 --height 300
# 反转颜色(白底黑字)
python image_converter.py image test_image.png output.py --invert
# 旋转90度
python image_converter.py image test_image.png output.py --rotate
# 不使用抖动算法
python image_converter.py image test_image.png output.py --no-dither
```
**重要提示:**
- 输出路径必须包含文件名,不能只是目录
- ❌ 错误: `python image_converter.py image input.png /path/to/directory`
- ✅ 正确: `python image_converter.py image input.png /path/to/directory/output.py`
### 2. 创建文本图片
```bash
python image_converter.py text <文本内容> <输出文件路径> [选项]
```
**示例:**
```bash
# 基本用法
python image_converter.py text "Hello World" output.py
# 指定字体大小
python image_converter.py text "Hello" output.py --font-size 32
# 白底黑字
python image_converter.py text "Hello" output.py --invert
```
### 3. 批量转换
```bash
python image_converter.py batch <输入目录> <输出目录> [选项]
```
**示例:**
```bash
python image_converter.py batch ./input_images ./output_files --width 400 --height 300
```
## 参数说明
### 通用参数
- `--width`: 目标宽度(默认400像素)
- `--height`: 目标高度(默认300像素)
- `--invert`: 反转颜色(黑底白字 → 白底黑字)
- `--rotate`: 旋转90度
### 图片转换专用
- `--no-dither`: 不使用抖动算法(使用简单阈值二值化)
### 文本创建专用
- `--font-size`: 字体大小(默认24)
## 在MicroPython中使用生成的文件
```python
# 导入生成的图片数据
from output import image_data
# 在image.py中使用
import image
image.run(image_data, width=400, height=300)
```
## 验证修复
运行验证脚本检查生成的文件是否正确:
```bash
python verify_fix.py
```
这会比较修复前后的数据,确认没有错误的换行符。
## 常见问题
### Q: 为什么显示乱码?
A: 如果使用旧版本转换器生成的文件,数据中包含错误的换行符。请使用修复后的转换器重新生成。
### Q: 如何重新生成所有图片?
A: 使用批量转换功能:
```bash
python image_converter.py batch ./old_images ./new_images --width 400 --height 300
```
### Q: 支持哪些图片格式?
A: 支持 JPG, PNG, BMP, GIF, TIFF 等常见格式。
### Q: 生成的文件大小是多少?
A: 对于400x300的图片,二进制数据大小为 15000 字节 (400 * 300 / 8)。
## 技术细节
### 数据格式
- 每个像素用1位表示(黑或白)
- 每行需要 `(width + 7) // 8` 个字节
- 总字节数: `width_bytes * height`
- 位顺序: MSB first (最高位在前)
### 正确的输出格式
```python
image_data = bytearray(b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'...')
```
### 错误的输出格式(已修复)
```python
# 不要使用这种格式!
image_data = bytearray(b'\n\xFF\xFF\xFF\xFF...\n\xFF\xFF...')
```

Binary file not shown.

Binary file not shown.

123
tool/convert_examples.py Normal file
View File

@@ -0,0 +1,123 @@
#!/usr/bin/env python3
"""
图片转换工具使用示例
"""
import os
import subprocess
import sys
def run_command(cmd):
"""运行命令并打印输出"""
print(f"执行命令: {' '.join(cmd)}")
result = subprocess.run(cmd, capture_output=True, text=True)
if result.stdout:
print(result.stdout)
if result.stderr:
print(f"错误: {result.stderr}")
return result.returncode == 0
def main():
print("=== 图片转换工具使用示例 ===\n")
# 检查是否存在示例图片目录
example_dir = "example_images"
output_dir = "converted_images"
if not os.path.exists(example_dir):
print(f"创建示例图片目录: {example_dir}")
os.makedirs(example_dir, exist_ok=True)
print("请将图片文件放入此目录后再次运行此脚本")
return
if not os.path.exists(output_dir):
print(f"创建输出目录: {output_dir}")
os.makedirs(output_dir, exist_ok=True)
# 示例1: 转换单个图片为黑色背景白色文本
print("\n示例1: 转换单个图片为黑色背景白色文本")
print("=" * 50)
cmd = [
"python3", "image_converter.py", "image",
os.path.join(example_dir, "example.jpg"),
os.path.join(output_dir, "example_dark.py"),
"--width", "400",
"--height", "300"
]
run_command(cmd)
# 示例2: 转换单个图片为白色背景黑色文本
print("\n示例2: 转换单个图片为白色背景黑色文本")
print("=" * 50)
cmd = [
"python3", "image_converter.py", "image",
os.path.join(example_dir, "example.jpg"),
os.path.join(output_dir, "example_light.py"),
"--width", "400",
"--height", "300",
"--invert"
]
run_command(cmd)
# 示例3: 创建文本图像
print("\n示例3: 创建文本图像")
print("=" * 50)
cmd = [
"python3", "image_converter.py", "text",
"Hello ESP32!",
os.path.join(output_dir, "hello_text.py"),
"--width", "400",
"--height", "300",
"--font-size", "32"
]
run_command(cmd)
# 示例4: 创建旋转90度的文本图像
print("\n示例4: 创建旋转90度的文本图像")
print("=" * 50)
cmd = [
"python3", "image_converter.py", "text",
"Rotated Text",
os.path.join(output_dir, "rotated_text.py"),
"--width", "400",
"--height", "300",
"--font-size", "32",
"--rotate"
]
run_command(cmd)
# 示例5: 批量转换图片
print("\n示例5: 批量转换图片")
print("=" * 50)
cmd = [
"python3", "image_converter.py", "batch",
example_dir,
output_dir,
"--width", "400",
"--height", "300"
]
run_command(cmd)
# 示例6: 转换为128x296尺寸与现有示例相同
print("\n示例6: 转换为128x296尺寸")
print("=" * 50)
cmd = [
"python3", "image_converter.py", "image",
os.path.join(example_dir, "example.jpg"),
os.path.join(output_dir, "example_128x296.py"),
"--width", "128",
"--height", "296",
"--rotate"
]
run_command(cmd)
print("\n=== 转换完成 ===")
print(f"输出文件位于: {output_dir}")
print("\n使用方法:")
print("1. 将转换后的.py文件复制到ESP32项目目录")
print("2. 在boot.py中设置RUN_MODE=3")
print("3. 修改image.py导入转换后的图片数据")
print("4. 上传代码到ESP32并运行")
if __name__ == '__main__':
main()

166
tool/example_usage.py Normal file
View File

@@ -0,0 +1,166 @@
#!/usr/bin/env python3
"""
示例如何在ESP32项目中使用转换后的图片数据
"""
# 假设你已经使用图片转换工具将图片转换为example_image.py
# 其中包含以下内容:
# example_image = bytearray(b'\x00\x00\x00\x0...')
# 1. 导入转换后的图片数据
# from example_image import example_image
# 2. 在image.py中使用图片数据
"""
# image.py 中的示例代码
import framebuf
from machine import Pin, SPI
import epaper4in2
import time
import config
def run(img_data=None):
# 如果没有提供图片数据,使用默认图片
if img_data is None:
# 可以在这里设置默认图片
img_data = None
try:
# 初始化墨水屏
epd = epaper4in2.EPD()
epd.init()
# 创建帧缓冲区
buf = bytearray(config.WIDTH * config.HEIGHT // 8)
fb = framebuf.FrameBuffer(buf, config.WIDTH, config.HEIGHT, framebuf.MONO_HLSB)
if img_data is not None:
# 使用提供的图片数据
# 根据图片数据长度判断图片尺寸
data_length = len(img_data)
# 常见尺寸检查
if data_length == 128 * 296 // 8: # 128x296
img_width, img_height = 128, 296
elif data_length == 400 * 300 // 8: # 400x300
img_width, img_height = 400, 300
else:
# 默认尺寸
img_width, img_height = 400, 300
# 计算居中位置
x_offset = (config.WIDTH - img_width) // 2
y_offset = (config.HEIGHT - img_height) // 2
# 将图片数据复制到帧缓冲区
for y in range(img_height):
for x in range(img_width):
# 计算源位置
src_byte_index = y * ((img_width + 7) // 8) + x // 8
src_bit_position = 7 - (x % 8)
# 获取像素值
pixel = (img_data[src_byte_index] >> src_bit_position) & 1
# 计算目标位置
dst_x = x_offset + x
dst_y = y_offset + y
if 0 <= dst_x < config.WIDTH and 0 <= dst_y < config.HEIGHT:
fb.pixel(dst_x, dst_y, pixel)
else:
# 如果没有图片数据,显示测试图案
fb.fill(0)
fb.rect(10, 10, config.WIDTH-20, config.HEIGHT-20, 1)
fb.text("No Image Data", 50, config.HEIGHT//2, 1)
# 显示图片
epd.display_frame(buf)
# 等待一段时间
time.sleep(5)
# 清屏并进入深度睡眠
epd.clear()
epd.sleep()
except Exception as e:
print(f"显示图片时出错: {e}")
# 清屏并进入深度睡眠
epd.clear()
epd.sleep()
"""
# 3. 在boot.py中设置RUN_MODE=3
"""
# boot.py 中的示例代码
import machine
import time
import config
# 设置运行模式
# 0: 正常运行模式
# 1: WiFi连接模式
# 2: 配置模式
# 3: 图片显示模式
RUN_MODE = 3
if RUN_MODE == 3:
# 导入图片数据
from example_image import example_image
# 导入图片显示模块
import image
# 显示图片
image.run(example_image)
"""
# 4. 使用不同的图片
"""
# 你可以根据需要导入不同的图片数据
# 显示黑色背景白色文本的图片
from example_dark import example_dark
image.run(example_dark)
# 显示白色背景黑色文本的图片
from example_light import example_light
image.run(example_light)
# 显示旋转的图片
from example_rotated import example_rotated
image.run(example_rotated)
# 显示文本图片
from hello_text import hello_text
image.run(hello_text)
"""
# 5. 动态选择图片
"""
# 你可以根据条件动态选择要显示的图片
def display_image_by_condition(condition):
if condition == "dark":
from example_dark import example_dark
return example_dark
elif condition == "light":
from example_light import example_light
return example_light
elif condition == "rotated":
from example_rotated import example_rotated
return example_rotated
else:
from hello_text import hello_text
return hello_text
# 使用示例
img_data = display_image_by_condition("dark")
image.run(img_data)
"""
print("这是一个示例文件展示了如何在ESP32项目中使用转换后的图片数据。")
print("请参考注释中的代码示例,根据你的实际需求进行修改。")

313
tool/image_converter.py Normal file
View File

@@ -0,0 +1,313 @@
#!/usr/bin/env python3
"""
图片转换工具 - 将多种格式的图片转换为适用于墨水屏显示的二进制点阵数据
支持转换为黑色背景白色文本或白色背景黑色文本的格式
"""
import os
import sys
from PIL import Image, ImageOps, ImageDraw, ImageFont
import argparse
import math
def convert_image_to_epaper(input_path, output_path, width=400, height=300, invert=False, rotate=False, dither=True):
"""
将图片转换为墨水屏二进制格式
参数:
input_path: 输入图片路径
output_path: 输出文件路径
width: 目标宽度(默认400)
height: 目标高度(默认300)
invert: 是否反转颜色(默认False黑色背景白色文本)
rotate: 是否旋转90度(默认False)
dither: 是否使用抖动算法(默认True)
"""
try:
# 打开图片
img = Image.open(input_path)
# 转换为RGB模式
if img.mode != 'RGB':
img = img.convert('RGB')
# 调整大小,保持宽高比
img_ratio = img.width / img.height
target_ratio = width / height
if img_ratio > target_ratio:
# 图片较宽,以宽度为准
new_width = width
new_height = int(width / img_ratio)
else:
# 图片较高,以高度为准
new_height = height
new_width = int(height * img_ratio)
img = img.resize((new_width, new_height), Image.LANCZOS)
# 创建目标大小的黑色背景
result = Image.new('RGB', (width, height), (0, 0, 0) if not invert else (255, 255, 255))
# 计算居中位置
x_offset = (width - new_width) // 2
y_offset = (height - new_height) // 2
# 将图片粘贴到中心
result.paste(img, (x_offset, y_offset))
# 转换为灰度
result = result.convert('L')
# 转换为1位黑白图像
if dither:
result = result.convert('1', dither=Image.FLOYDSTEINBERG)
else:
# 使用阈值128进行二值化
result = result.point(lambda x: 0 if x < 128 else 255, '1')
# 如果需要反转颜色
if invert:
result = ImageOps.invert(result)
# 如果需要旋转90度
if rotate:
result = result.rotate(90, expand=True)
# 如果旋转后尺寸不匹配,需要重新调整
if result.size != (width, height):
result = result.resize((width, height), Image.LANCZOS)
# 转换为字节数组
width_bytes = (width + 7) // 8 # 每行需要的字节数
total_bytes = width_bytes * height
# 创建字节数组
byte_array = bytearray(total_bytes)
# 将像素数据转换为字节数组
for y in range(height):
for x in range(width):
# 获取像素值 (0或255)
pixel = 0 if result.getpixel((x, y)) == 0 else 1
# 计算字节位置和位位置
byte_index = y * width_bytes + x // 8
bit_position = 7 - (x % 8) # 最高位在前
# 设置位
if pixel:
byte_array[byte_index] |= (1 << bit_position)
# 生成Python代码
var_name = os.path.splitext(os.path.basename(output_path))[0]
with open(output_path, 'w', encoding='utf-8') as f:
f.write(f"# Converted from {input_path}\n")
f.write(f"# Size: {width}x{height}\n")
f.write(f"# Inverted: {invert}, Rotated: {rotate}\n")
f.write(f"{var_name} = bytearray(b'")
# 将字节数组格式化为十六进制字符串
for i, byte in enumerate(byte_array):
if i > 0 and i % 16 == 0:
f.write("'\n b'")
f.write(f"\\x{byte:02X}")
f.write("')\n")
print(f"转换完成: {input_path} -> {output_path}")
print(f"输出尺寸: {width}x{height}")
print(f"总字节数: {total_bytes}")
return True
except Exception as e:
print(f"转换失败: {e}")
return False
def create_text_image(text, output_path, width=400, height=300, font_size=24, invert=False, rotate=False):
"""
创建文本图像并转换为墨水屏二进制格式
参数:
text: 要显示的文本
output_path: 输出文件路径
width: 目标宽度(默认400)
height: 目标高度(默认300)
font_size: 字体大小(默认24)
invert: 是否反转颜色(默认False黑色背景白色文本)
rotate: 是否旋转90度(默认False)
"""
try:
# 创建图像
result = Image.new('RGB', (width, height), (0, 0, 0) if not invert else (255, 255, 255))
draw = ImageDraw.Draw(result)
# 尝试加载字体
try:
# 尝试使用系统字体
font = ImageFont.truetype("arial.ttf", font_size)
except:
try:
# 尝试使用项目中的字体
font = ImageFont.truetype("GB2312-12.fon", font_size)
except:
# 使用默认字体
font = ImageFont.load_default()
# 计算文本位置
text_width, text_height = draw.textsize(text, font=font)
x = (width - text_width) // 2
y = (height - text_height) // 2
# 绘制文本
text_color = (255, 255, 255) if not invert else (0, 0, 0)
draw.text((x, y), text, font=font, fill=text_color)
# 转换为灰度
result = result.convert('L')
# 转换为1位黑白图像
result = result.convert('1')
# 如果需要旋转90度
if rotate:
result = result.rotate(90, expand=True)
# 如果旋转后尺寸不匹配,需要重新调整
if result.size != (width, height):
result = result.resize((width, height), Image.LANCZOS)
# 转换为字节数组
width_bytes = (width + 7) // 8 # 每行需要的字节数
total_bytes = width_bytes * height
# 创建字节数组
byte_array = bytearray(total_bytes)
# 将像素数据转换为字节数组
for y in range(height):
for x in range(width):
# 获取像素值 (0或255)
pixel = 0 if result.getpixel((x, y)) == 0 else 1
# 计算字节位置和位位置
byte_index = y * width_bytes + x // 8
bit_position = 7 - (x % 8) # 最高位在前
# 设置位
if pixel:
byte_array[byte_index] |= (1 << bit_position)
# 生成Python代码
var_name = os.path.splitext(os.path.basename(output_path))[0]
with open(output_path, 'w', encoding='utf-8') as f:
f.write(f"# Text image: {text}\n")
f.write(f"# Size: {width}x{height}\n")
f.write(f"# Font size: {font_size}\n")
f.write(f"# Inverted: {invert}, Rotated: {rotate}\n")
f.write(f"{var_name} = bytearray(b'")
# 将字节数组格式化为十六进制字符串
for i, byte in enumerate(byte_array):
if i > 0 and i % 16 == 0:
f.write("'\n b'")
f.write(f"\\x{byte:02X}")
f.write("')\n")
print(f"文本图像创建完成: {output_path}")
print(f"文本: {text}")
print(f"输出尺寸: {width}x{height}")
print(f"总字节数: {total_bytes}")
return True
except Exception as e:
print(f"创建失败: {e}")
return False
def main():
parser = argparse.ArgumentParser(description='将图片转换为墨水屏二进制格式')
subparsers = parser.add_subparsers(dest='command', help='子命令')
# 图片转换命令
img_parser = subparsers.add_parser('image', help='转换图片文件')
img_parser.add_argument('input', help='输入图片路径')
img_parser.add_argument('output', help='输出文件路径')
img_parser.add_argument('--width', type=int, default=400, help='目标宽度(默认400)')
img_parser.add_argument('--height', type=int, default=300, help='目标高度(默认300)')
img_parser.add_argument('--invert', action='store_true', help='反转颜色(白色背景黑色文本)')
img_parser.add_argument('--rotate', action='store_true', help='旋转90度')
img_parser.add_argument('--no-dither', action='store_true', help='不使用抖动算法')
# 文本创建命令
text_parser = subparsers.add_parser('text', help='创建文本图像')
text_parser.add_argument('text', help='要显示的文本')
text_parser.add_argument('output', help='输出文件路径')
text_parser.add_argument('--width', type=int, default=400, help='目标宽度(默认400)')
text_parser.add_argument('--height', type=int, default=300, help='目标高度(默认300)')
text_parser.add_argument('--font-size', type=int, default=24, help='字体大小(默认24)')
text_parser.add_argument('--invert', action='store_true', help='反转颜色(白色背景黑色文本)')
text_parser.add_argument('--rotate', action='store_true', help='旋转90度')
# 批量转换命令
batch_parser = subparsers.add_parser('batch', help='批量转换图片')
batch_parser.add_argument('input_dir', help='输入目录')
batch_parser.add_argument('output_dir', help='输出目录')
batch_parser.add_argument('--width', type=int, default=400, help='目标宽度(默认400)')
batch_parser.add_argument('--height', type=int, default=300, help='目标高度(默认300)')
batch_parser.add_argument('--invert', action='store_true', help='反转颜色(白色背景黑色文本)')
batch_parser.add_argument('--rotate', action='store_true', help='旋转90度')
batch_parser.add_argument('--no-dither', action='store_true', help='不使用抖动算法')
args = parser.parse_args()
if args.command == 'image':
convert_image_to_epaper(
args.input,
args.output,
args.width,
args.height,
args.invert,
args.rotate,
not args.no_dither
)
elif args.command == 'text':
create_text_image(
args.text,
args.output,
args.width,
args.height,
args.font_size,
args.invert,
args.rotate
)
elif args.command == 'batch':
# 确保输出目录存在
os.makedirs(args.output_dir, exist_ok=True)
# 支持的图片格式
supported_formats = ('.jpg', '.jpeg', '.png', '.bmp', '.gif', '.tiff')
# 遍历输入目录
for filename in os.listdir(args.input_dir):
if filename.lower().endswith(supported_formats):
input_path = os.path.join(args.input_dir, filename)
output_filename = os.path.splitext(filename)[0] + '.py'
output_path = os.path.join(args.output_dir, output_filename)
convert_image_to_epaper(
input_path,
output_path,
args.width,
args.height,
args.invert,
args.rotate,
not args.no_dither
)
else:
parser.print_help()
if __name__ == '__main__':
main()

BIN
tool/test_iamge.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

941
tool/test_iamge_fixed.py Normal file
View File

@@ -0,0 +1,941 @@
# Converted from tool/test_iamge_white_bg.png
# Size: 400x300
# Inverted: False, Rotated: False
test_iamge_fixed = bytearray(b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\x8F\xFF\xFF\xFF\xFF\xFC\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFC\xFF\x87\xFF\xFF\xFF\xFF\xF8\xFF\x3F\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFC\x7F\x87\xFF\xFF\xFF\xFF\xF8\xF8\x1F\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFD\x3F\x06\x8F\xFF\xFF\xF8\xF8\x40\x8F'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFC\x3A\x10\x07\xFF\xFF\xF8\xF2'
b'\x02\x5F\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFE\x00\x00\x27\xFF\xFF'
b'\xF8\x00\x0B\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xE8\x00\x81\x57'
b'\xFF\xFF\xF8\x01\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFC\x00\x48'
b'\x3F\xFF\xFF\xFC\x81\x11\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFC'
b'\x00\x02\xFF\xFF\xFF\xF8\x00\x77\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFE\x49\x38\xFF\xFF\xFF\xF0\x04\xF3\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\x06\x51\x3F\xFF\xFF\xFD\x2C\xF7\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x40\x1F\xFF\xFF\xFF\xFD\xE7\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFE\x40\x1F\xFF\xFF\xFF\xFE\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x09\x7F\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xF8\x00\x3F\xFF'
b'\xFF\xFF\xF3\xBF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xF8\x22'
b'\x1F\xFF\xFF\xFF\xE3\x8F\x8F\xCF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFC\x80\x4F\xFF\xFF\xFF\xE3\x87\x8F\x8F\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xC4\x1F\xFF\xFF\xFF\xC3\x87\xC3\x1F\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFC\x51\x1F\xFF\xFF\xFF\xD7\x85\xC2\x3F\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFC\x44\x3E\x3F\xFF\xFF\xC7\x94\x48\x7F'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFC\x00\x20\x0F\xFF\xFF\x81\x80'
b'\x40\x7F\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFD\x10\x40\x87\xFF\xFF'
b'\x19\x84\x44\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFC\x01\x04\x0F'
b'\xFF\xFF\x00\x94\x4C\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFE\x44'
b'\x03\xFF\xFF\xFE\x40\x0F\xCC\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFC\x00\x2F\xFF\xFF\xFE\x04\x3F\x98\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xC0\x10\xFF\xFF\xFF\xFD\x90\xB1\xD9\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xF0\x00\x87\xC1\xFF\xFF\xFF\x00\x63\x79\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xF0\x02\x3E\x00\x3F\xFF\xFE\x21\x87\x19\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xF2\x25\xF0\x08\x1F\xFF\xFE\x77\x0F'
b'\xD1\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xF0\x3F\x80\x20\x1F\xFF\xFC'
b'\x78\x2F\xC3\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFD\xCC\x02\x7D\x1F'
b'\xFF\xFC\x70\x33\xE3\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x80\x15'
b'\x3E\x1F\xFF\xF9\x39\x43\xE3\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xC4\x98\x3C\x5F\xFF\xF3\x1E\x03\xE3\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xC1\x80\x3E\x1F\xFF\xE7\x3E\x4B\xC1\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xD3\x82\x3C\x1F\xFF\xEF\x3E\x23\xC8\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xE3\xD8\x7C\x1F\xFF\xDE\x1E\x47\x98\x7F'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xE3\x80\xFC\x5F\xFF\xDE\x3E\x53'
b'\x39\x3F\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xE3\xC3\xF8\x1F\xFF\xFF'
b'\x1E\x44\x7C\x0F\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xE7\x87\xFA\x3F'
b'\xFF\xFE\x1E\x40\xFE\x03\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xE7\xD0'
b'\xF8\x3F\xFF\xFF\x1E\x83\xFE\x23\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xF7\xC0\x28\xBF\xFF\xFE\x0C\xC9\xFF\x00\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xE4\x0B\x00\x7F\xFF\xFE\x2E\x83\xFF\x80\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xE1\x0F\x82\x7F\xFF\xFE\x0D\xC7\xFF\xC8\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xF0\x3F\xC8\x7F\xFF\xFE\x4B\xCF\xFF\xF0'
b'\x7F\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFE\xFF\xE0\xFF\xFF\xFF\x1F\xFF'
b'\xFF\xFE\x7F\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xF1\xFF\xFF\xFF'
b'\xBF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFC\x3F'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFC\x3F\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFD\xFD\x3F\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFC\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xF8\x7C\x17\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFC\x1F\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xF8\x3C\x00\xFF\xFF\xFF\xFF\xFE\x3F\xFF\xFF'
b'\xFF\xFF\xFC\x0F\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xF8\x70\x40\xFF\xFF\xFF\xFF\xFE\x3F'
b'\xFF\xFF\xFF\xFF\xFC\x2F\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xF5\xFF\xFF\xFF\xFF\xFF\xFF\xFA\x01\x09\xFF\xFF\xFF\xFF'
b'\xFE\x3F\xFF\xFF\xFF\xFF\xF8\x07\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFC\x7F\xFF\xFF\xF0\x7F\xFF\xFF\xFF\xFF\xFF\xE0\x10\xFF\xFF\xFF'
b'\xFF\x1F\xFF\x3F\xFF\xFF\xFF\xFF\xF0\x8F\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xF8\x3F\xFF\xFF\xF0\x3F\xFF\xFF\xFF\xFF\xFF\x01\x11\xFF'
b'\xFF\xFF\xFF\x0F\xFF\x3F\xFF\xFF\xFF\xFF\xE0\x3F\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xE1\x3F\xFF\xFF\xF9\x3F\xFF\xFF\xFF\xFF\xFC\x10'
b'\x71\xFF\xFF\xFF\xFF\x0F\xFE\x3E\x3F\xFF\xFF\xFF\xC2\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xC4\x7F\xFF\xFF\xFC\x3F\xFF\xFC\xFF\xFF'
b'\xFE\x0C\x73\xFF\xFF\xFF\xFF\x87\xFE\x78\x1F\xFF\xFF\xFF\x83\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x10\x7F\xFF\xFF\xFE\x3F\xFF\xF8'
b'\x3F\xFF\xFF\xFC\x63\xFF\xFF\xFF\xFF\x87\xFE\x20\x9F\xFF\xFF\xFF'
b'\x1F\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xEC\x40\xFF\xFF\xFF\xFF\x7F'
b'\xEF\xF0\x3F\xFF\xFF\xFC\x67\xFF\xFF\xFF\xFF\x8F\xFE\x02\x1F\xFF'
b'\xFF\xFE\x3F\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xE0\x01\xFF\xFF\xFF'
b'\xFF\x7F\x83\xF1\x7F\xFF\xFF\xFC\xE7\xFF\xFF\xFF\xFF\x0F\xFE\x88'
b'\x3F\xFF\xFF\xFE\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xF1\x07\xFF'
b'\xFF\xFF\xFF\xFF\x03\xE1\x7F\xFF\xFF\xFC\x6F\xFF\xFF\xFF\xFF\xA7'
b'\xFC\x10\xFF\xFF\xFF\xE7\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\x93\xFF\xFF\xFF\xFF\xFC\x27\xC7\xFF\xFF\xFF\xFE\xCF\xFA\x3F\xFF'
b'\xFF\x8F\xF0\x51\xFF\xFF\xFF\xC3\xFF\xF9\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\x07\xFF\xFF\xFF\xFF\xF0\x07\x83\xFF\xFF\xFF\xFC\xFF\xC0'
b'\x3F\xFF\xFF\x87\xFE\x03\xFF\xFF\xFF\xF1\xF9\xE1\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\x0F\xFF\xFF\xFF\xFF\xF0\x0F\x03\xFF\xFF\xFF\xFE'
b'\xFE\x00\x1F\xFF\xFF\x8F\xFE\x47\xFF\xFF\xFF\xF8\xF1\xC5\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFC\x1E\x3F\xFF\xFF\xFF\xFF\xFD\x4E\x69\xFF\xFF'
b'\xFF\xFE\xF0\x3D\x3F\xFF\xFF\x0F\xFC\x07\xFF\xFF\xFF\xFC\xF8\x07'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xF9\x00\x3F\xFF\xFF\xFF\xF0\xFF\x1F\xE3'
b'\xFF\xFF\xFF\xFF\xC1\x7C\x1F\xFF\xFF\x8F\xFE\x27\xFF\xFF\xFF\xF8'
b'\xF8\x17\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xF2\x7F\xFF\xFF\xFF\xC0\x7F'
b'\x01\xE3\xFF\xFF\xFF\xFF\x05\xF8\x3F\xFF\xCF\x8C\xFC\x30\xFF\xFF'
b'\xFF\xFC\xFA\x4F\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xF8\x0F\xFF\xFF\xFF'
b'\x80\xFE\x23\xE0\x3F\xFF\xFF\xF8\x0F\xF8\xBF\xFF\xCF\x81\xFD\x70'
b'\xFF\xFF\xFF\xF8\x70\x3F\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xF8\x8F\xFF'
b'\xFF\xFE\x12\xFE\x03\xE0\x7F\xFF\xFF\xC0\x27\xF0\x7F\xFF\xE7\x13'
b'\xFC\x61\xFF\xFF\xFF\xF8\xC0\x7F\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xF1'
b'\xC1\xFF\xFF\xFC\x00\xFD\x0B\xE4\xFF\xFF\xFE\x04\x8F\xF2\x7F\xFF'
b'\xE7\x87\xF8\xC5\xFF\xFF\xFF\xF0\x04\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xE3\xF0\xFF\xFF\xFC\x80\xFC\x20\xE1\xFF\xFF\xFE\x11\xC7\xE0'
b'\xFF\xFF\xC3\x0F\xF0\x83\xFF\xFF\xFF\xF4\x90\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xE3\xF8\x3F\xFF\xFE\x35\xFE\x08\xE7\xFF\xFF\xFF\x07'
b'\x87\xE0\xFF\xFF\xC0\x2F\xE2\x0F\xFF\xFF\xFF\xF0\x00\x7F\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xC7\x7C\x0F\xFF\xFF\xF0\xFF\x98\xE7\xFF\xFF'
b'\xFF\xFF\xC9\xC9\xFF\xFF\xC8\x9F\xC0\x1F\xFF\xFF\xFF\xE0\x02\x7F'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x8F\x2E\x81\xFF\xFF\xF1\xFF\x9C\xE7'
b'\xFF\xFF\xFF\xFF\xC8\x43\xFF\xFF\x80\x1F\xC4\x7F\xFF\xFF\xFF\xE2'
b'\x48\x7F\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x1E\x03\x00\x0F\xFF\xE0\x0F'
b'\x1C\xEE\x01\xFF\xFF\xFF\xC4\x03\xFF\xFF\xC2\x1F\xE0\xFF\xFF\xFF'
b'\xFF\xE0\x38\x7F\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x3E\x03\xC4\x07\xFF'
b'\xE0\x03\x3C\x40\x00\xFF\xFF\xFF\xC6\x13\xFF\xFF\xC9\x9F\xF0\xFF'
b'\xFF\xFF\xFF\xF0\xF8\x43\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFE\x7F\x27\xE0'
b'\x03\xFF\xE9\xF0\x30\x00\x91\xFF\xFF\xFF\xC7\x47\xFF\xFF\xC3\x1F'
b'\xF9\x3F\xFF\xFF\xFF\xF2\xF9\x60\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFC\x78'
b'\x07\xF0\x91\xFF\xE1\xCC\x60\x15\x4F\xFF\xFF\xFF\x87\x8F\xFF\xFF'
b'\xEF\xBF\xF3\x1F\xFF\xFF\xFF\xFF\xF8\x78\x0F\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xF0\xE0\x1F\xFC\x03\xFF\xE1\x9C\x1B\xFF\xFF\xFF\xFF\xFF\xC7\xFF'
b'\xFF\xFF\xFF\x1F\xF3\x8F\xFF\xFF\xFF\xFF\xF0\x7E\x01\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xE3\xE1\x3F\xFF\xFF\xFF\xE5\x3C\x0F\xFF\xFF\xFF\xFF\xFF'
b'\xC7\xFF\xFF\xFF\xFF\x9F\xF3\xC3\xFF\xFF\xFF\xFF\xF9\x7F\x90\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\x0B\xF0\x3F\xFF\xFF\xFF\xE2\x39\xA7\xFF\xFF\xFF'
b'\xFF\xFF\xC7\xFF\xFF\xFF\xFF\x1F\xE7\xE3\xFF\xFF\xFF\xFF\xF0\x7F'
b'\x02\x7F\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\x9F\xFE\x7C\x07\xFF\xFF\xE0\x3B\xC1\xFF'
b'\xFF\xFF\xFF\xFF\x87\xFF\xFF\xFF\xFF\x9F\xE7\xE0\xFF\xFF\xFF\xFF'
b'\xF0\x7F\x08\x3F\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFE\x60\x03\xFF\xFF\xC0\xF7'
b'\xE0\xFF\xFF\xFF\xFF\xFF\xC3\xFF\xFF\xFF\xFF\x9F\xCF\xF0\x3F\xFF'
b'\xFF\xE0\x24\x7F\xE0\x3F\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x00\x93\xFF\xFF'
b'\xC8\x6F\xF0\x3F\xFF\xFF\xFF\xFF\x97\xFF\xFF\xFF\xFF\x9F\xCF\xF8'
b'\x1F\xFF\xEF\xF4\x80\x7F\xF0\x3F\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFE\x12\x03'
b'\xFF\xFF\xC1\xDF\xFC\x06\xFF\xFF\xFF\xFF\xC3\xFF\xFF\xFF\xFF\x3F'
b'\x9F\xFC\x83\xFF\xE7\xFE\x01\x7F\xFC\xBF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xF8'
b'\x1C\x0F\xFF\xFF\xC1\xBF\xFE\x00\x3F\xFF\xFF\xFF\x83\xFF\xFF\xFF'
b'\xFF\x9F\x3F\xFE\x01\xFF\xE7\xFF\x88\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xE0\xFF\xFF\xFF\xFF\xE7\xFF\xFF\x48\x03\xFF\xFF\xFF\xC7\xFF'
b'\xFF\xFF\xFE\x1E\xFF\xFE\x08\x7F\xE7\xFF\xE0\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\x85\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x80\x80\x7F\xFF\xFF'
b'\xC3\xFF\xFF\xFF\xFF\x1F\xFF\xFF\xA0\x7F\xC6\xFF\xF3\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\x57\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xF0\x05\xFF'
b'\xFF\xFF\xC7\xFF\xFF\xFF\xFF\x1F\xFF\xFF\xD0\x7F\xE0\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\x83\xFF\xFF\xFF\xFF\x1F\xFF\xFF\xFF\xFF\xC9\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xCB\xFF\xFF\xFF\xFF\x5F\xFF\xFF\xFF\xFF'
b'\xC1\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xC7\xFF\xFF\xFF\xFF\x9F\xFF\xFF'
b'\xFF\xFF\xD3\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xC7\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xE3\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xC7\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xF7\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xEF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xEF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF')

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

46
tool/verify_fix.py Normal file
View File

@@ -0,0 +1,46 @@
#!/usr/bin/env python3
"""
验证修复后的图片数据
"""
# 导入旧的(有问题的)和新的(修复后的)图片数据
from test_iamge import test_iamge as old_data
from test_iamge_fixed import test_iamge_fixed as new_data
print("=== 验证图片数据修复 ===\n")
# 检查数据长度
print(f"旧数据长度: {len(old_data)} 字节")
print(f"新数据长度: {len(new_data)} 字节")
print(f"预期长度: {(400 * 300) // 8} 字节\n")
# 检查旧数据中的换行符
newline_count = old_data.count(0x0A) # 0x0A 是换行符 \n
print(f"旧数据中的换行符(0x0A)数量: {newline_count}")
print(f"新数据中的换行符(0x0A)数量: {new_data.count(0x0A)}\n")
# 显示前几个字节
print("旧数据前32字节:")
print(' '.join(f'{b:02X}' for b in old_data[:32]))
print("\n新数据前32字节:")
print(' '.join(f'{b:02X}' for b in new_data[:32]))
# 检查数据类型
print(f"\n旧数据类型: {type(old_data)}")
print(f"新数据类型: {type(new_data)}")
print("\n=== 结论 ===")
if newline_count > 0:
print(f"❌ 旧数据包含 {newline_count} 个错误的换行符,会导致显示乱码")
else:
print("✓ 旧数据没有换行符问题")
if new_data.count(0x0A) == 0 or new_data.count(0x0A) < newline_count:
print("✓ 新数据已修复,不包含错误的换行符")
else:
print("❌ 新数据仍有问题")
if len(new_data) == 15000:
print("✓ 新数据长度正确 (400x300/8 = 15000 字节)")
else:
print(f"❌ 新数据长度不正确,应该是 15000 字节")