Initial commit

This commit is contained in:
Oliver Traber 2023-09-21 14:56:01 +02:00
commit b9d5c06956
Signed by: Bluemedia
GPG key ID: C0674B105057136C
55 changed files with 4706 additions and 0 deletions

0
backend/app/__init__.py Normal file
View file

View file

View file

@ -0,0 +1,12 @@
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
SQLALCHEMY_DATABASE_URL = "sqlite:///./scanner.db"
engine = create_engine(
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()

View file

@ -0,0 +1 @@
from .database import Base

View file

@ -0,0 +1,13 @@
from pydantic import BaseModel
import app.scanner.enums as scan
class ScanPage(BaseModel):
filename: str
size_bytes: int
class Config():
orm_mode = True
class ScanStatus(BaseModel):
pages: list[ScanPage]
status: scan.Status

43
backend/app/main.py Normal file
View file

@ -0,0 +1,43 @@
import threading
from contextlib import asynccontextmanager
from typing import Annotated
from fastapi import FastAPI, Depends
from app.data import models
from app.data.database import SessionLocal, engine
from app.scanner.scanner import Scanner
from app.scanner.scanner import Status as ScannerStatus
models.Base.metadata.create_all(bind=engine)
__scanner = Scanner("/var/www/html/img")
@asynccontextmanager
async def __lifespan(app: FastAPI):
threading.Thread(target=__scanner.preload).start()
yield
app = FastAPI(lifespan=__lifespan)
# SQLAlchemiy session dependency
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
def get_scanner():
return __scanner
from app.routers import power
app.include_router(power.router)
from app.routers import scan
app.include_router(scan.router)
@app.get("/api/ready")
async def __ready(scanner: Annotated[Scanner, Depends(get_scanner)]):
return scanner.get_status() != ScannerStatus.INITIALIZED

View file

View file

@ -0,0 +1,14 @@
from fastapi import APIRouter
import subprocess
router = APIRouter(prefix="/api/power")
@router.post("/shutdown")
async def power_shutdown():
subprocess.call(["sudo", "shutdown", "-h", "now"])
return {}
@router.post("/restart")
async def power_restart():
subprocess.call(["sudo", "reboot"])
return {}

View file

@ -0,0 +1,23 @@
from typing import Annotated
from app.scanner.scanner import Scanner
from app.main import get_scanner
from fastapi import APIRouter, Depends
from app.data import schemas, models
router = APIRouter(prefix="/api/scan")
@router.post("")
async def scan(scanner: Annotated[Scanner, Depends(get_scanner)]):
scanner.scan()
return []
@router.get("/status", response_model=schemas.ScanStatus)
async def status(scanner: Annotated[Scanner, Depends(get_scanner)]):
pages = [schemas.ScanPage.from_orm(page) for page in scanner.get_pages()]
return schemas.ScanStatus(pages=pages,status=scanner.get_status())
@router.get("/debug")
async def debug(scanner: Annotated[Scanner, Depends(get_scanner)]):
return scanner.get_options()

View file

View file

@ -0,0 +1,22 @@
from enum import Enum
class Status(Enum):
INITIALIZED = "initialized"
IDLE = "idle"
RUNNING = "running"
DONE = "done"
ERR_NO_PAPER = "err_no_paper"
ERR_COVER_OPEN = "err_cover_open"
class Setting(Enum):
PAPER_SOURCE = "source"
COLOR_MODE = "color"
RESOLUTION = "resolution"
PAPER_SIZE = "paper_size"
class PaperSize(Enum):
A3 = "a3"
B3 = "b3"
A4 = "a4"
B4 = "b4"
LETTER = "letter"

View file

@ -0,0 +1,140 @@
import gi, os, threading
from typing import List
from PIL import Image
from app.scanner.enums import Status
gi.require_version('Libinsane', '1.0')
from gi.repository import Libinsane, GObject # type: ignore
class __LibinsaneSilentLogger(GObject.GObject, Libinsane.Logger):
def do_log(self, lvl, msg):
return
Libinsane.register_logger(__LibinsaneSilentLogger())
class Page:
filename: str
size_bytes: int
class Scanner:
def __get_device_id(self):
"""
List local scanners and get the device id of the first found device.
:param self: Instance of this class
:returns: Device id of the first scan device
"""
devs = self.api.list_devices(Libinsane.DeviceLocations.LOCAL_ONLY)
return devs[0].get_dev_id()
def __raw_to_img(self, params, img_bytes):
"""
"""
fmt = params.get_format()
assert(fmt == Libinsane.ImgFormat.RAW_RGB_24)
(w, h) = (
params.get_width(),
int(len(img_bytes) / 3 / params.get_width())
)
return Image.frombuffer("RGB", (w, h), img_bytes, "raw", "RGB", 0, 1)
def __write_file(self, scan_params, data, page_index, last_file):
data = b"".join(data)
if scan_params.get_format() == Libinsane.ImgFormat.RAW_RGB_24:
filesize = len(data)
img = self.__raw_to_img(scan_params, data)
filename = f"out{page_index}.png"
img.save(os.path.join(self.storage_path, filename), format="PNG")
page = Page()
page.filename = filename
page.size_bytes = filesize
self.scanned_pages.append(page)
if last_file:
self.status = Status.DONE
def __set_defaults(self):
dev = self.api.get_device(self.device_id)
opts = dev.get_options()
opts = {opt.get_name(): opt for opt in opts}
opts["sleeptimer"].set_value(1)
opts["resolution"].set_value(200)
dev.close()
def __scan(self):
self.status = Status.RUNNING
source = self.api.get_device(self.device_id)
opts = source.get_options()
opts = {opt.get_name(): opt for opt in opts}
if opts["cover-open"].get_value() == True:
self.status = Status.ERR_COVER_OPEN
return
session = source.scan_start()
try:
page_index = 0
while not session.end_of_feed() and page_index < 50:
# Do not assume that all the pages will have the same size !
scan_params = session.get_scan_parameters()
img = []
while not session.end_of_page():
data = session.read_bytes(256 * 1024)
data = data.get_data()
img.append(data)
t = threading.Thread(target=self.__write_file, args=(scan_params, img, page_index, session.end_of_feed()))
t.start()
page_index += 1
if page_index == 0:
self.status = Status.ERR_NO_PAPER
finally:
session.cancel()
source.close()
def __init__(self, storage_path):
self.scanned_pages: List[Page] = []
self.storage_path = storage_path
self.status = Status.INITIALIZED
def preload(self):
os.environ["LIBINSANE_NORMALIZER_SAFE_DEFAULTS"] = "0"
self.api = Libinsane.Api.new_safebet()
self.device_id = self.__get_device_id()
self.__set_defaults()
self.status = Status.IDLE
def scan(self):
if self.status == Status.RUNNING:
raise RuntimeError("already_running")
if self.status == Status.INITIALIZED:
self.preload()
self.scanned_pages: List[Page] = []
t = threading.Thread(target=self.__scan)
t.start()
def get_status(self) -> Status:
return self.status
def get_pages(self) -> List[Page]:
return self.scanned_pages
def get_options(self):
dev = self.api.get_device(self.device_id)
opts = dev.get_options()
result = {}
for opt in opts:
try:
result[opt.get_name()] = opt.get_value()
except Exception:
continue
dev.close()
return result
def cleanup(self):
if self.status == Status.RUNNING:
raise RuntimeError("scan_running")
if self.status != Status.INITIALIZED:
self.api.cleanup()