diff --git a/.drone.yml b/.drone.yml
new file mode 100644
index 0000000..5b14c9b
--- /dev/null
+++ b/.drone.yml
@@ -0,0 +1,29 @@
+kind: pipeline
+name: default
+steps:
+ - name: container-build
+ image: 'docker:dind'
+ environment:
+ USERNAME:
+ from_secret: docker_username
+ PASSWORD:
+ from_secret: docker_password
+ volumes:
+ - name: dockersock
+ path: /var/run/docker.sock
+ commands:
+ - >-
+ if [ "${DRONE_BRANCH}" = "main" ]; then export
+ BUILD_TAG="latest"; else export BUILD_TAG="${DRONE_BRANCH}"; fi;
+ - docker login -u $USERNAME -p $PASSWORD git.argideli.com
+ - >-
+ docker build -t
+ git.argideli.com/${DRONE_REPO_NAMESPACE}/${DRONE_REPO_NAME}:$BUILD_TAG
+ .
+ - >-
+ docker push
+ git.argideli.com/${DRONE_REPO_NAMESPACE}/${DRONE_REPO_NAME}:$BUILD_TAG
+volumes:
+ - name: dockersock
+ host:
+ path: /var/run/docker.sock
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..68bc17f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,160 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+# For a library or package, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# poetry
+# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+# in version control.
+# https://pdm.fming.dev/#use-with-ide
+.pdm.toml
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+# and can be added to the global gitignore or merged into this file. For a more nuclear
+# option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..a780005
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,25 @@
+# Use the official Python base image
+FROM python:3.11-slim
+
+# Set the working directory inside the container
+WORKDIR /code
+
+# Copy the requirements file to the working directory
+COPY ./requirements.txt /code/requirements.txt
+
+# Install the Python dependencies
+RUN pip config --user set global.progress_bar off
+RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
+
+# Copy the application code to the working directory
+COPY ./app /code/app
+COPY ./app/dot_env /code/app/.env
+COPY ./app/favicon.ico /code/favicon.ico
+COPY ./app/favicon.ico /code/app/favicon.ico
+
+# Expose the port on which the application will run
+EXPOSE 80
+
+# Run the FastAPI application using uvicorn server
+#CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"]
+CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80"]
diff --git a/README.md b/README.md
index 77b917e..08e1ab6 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,45 @@
-# qr-api
+# Python QR Codes API
+Python API for adding QR Codes in invoices
+
+# Deployment
+
+docker-compose up
+
+The signed invoices are saved at the invoices folder, their filenames are inv_code.pdf
+
+# Example Calls
+## Sign with QR
+HTTP-POST
+
+https://qrcode.argideli.com/signinvoice/?company=Quertex&inv_type=tpy&in_all=0&preset=inv2&inv_series=2&inv_num=5578
+
+The call body is a multipart form-data, containing the actual invoice. Upon signing with the QR code the new file is returned
+
+Parameters
+
+* file: UploadFile = File(...) = File to be signed
+* preset: str = Invoice Preset Selection
+* code_content: str = QR code Data, if empty the code data that will be put inside the QR will be the url to download the invoice
+* company: str = Invoice type, english letters (ex. Quertex), NOTE: The Upper Case letter WILL be carried to the filename
+* inv_type: str = Invoice type, english letters (ex. tpy)
+* inv_series: str = Invoice series, english letters (ex. 2 or B1)
+* inv_num: str = Invoice Number
+* in_all: bool = Sign all pages (0|1)
+
+
+## Get invoice
+HTTP-GET
+
+https://qrcode.argideli.com/getinvoice/?inv_code=Quertex_tpy_2_5578
+
+Get a saved invoice by calling an HTTP/GET and providing the filename as the inv_code parameter, alternatevily the identifier can be reconstructed from the company, inv_type, inv_series, inv_num parameters
+
+Parameters
+
+* inv_code: str = Invoice identifier/Filename without .pdf extention
+* company: str = Invoice type, english letters (ex. Quertex), NOTE: The Upper Case letter WILL be carried to the filename
+* inv_type: str = Invoice type, english letters (ex. tpy)
+* inv_series: str = Invoice series, english letters (ex. 2 or B1)
+* inv_num: str = Invoice Number
+* search_file: bool = Get file by reconstructing the filename from (company, inv_type, inv_series, inv_num), (0|1)
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 0000000..68bc17f
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1,160 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+# For a library or package, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# poetry
+# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+# in version control.
+# https://pdm.fming.dev/#use-with-ide
+.pdm.toml
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+# and can be added to the global gitignore or merged into this file. For a more nuclear
+# option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
diff --git a/app/__init__.py b/app/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/dot_env b/app/dot_env
new file mode 100644
index 0000000..07811f4
--- /dev/null
+++ b/app/dot_env
@@ -0,0 +1,7 @@
+SERVICE_URL= "https://qrcode.argideli.com"
+DESTINATION = "/var/invoices/"
+
+#SERVICE_URL= "http://127.0.0.1:8000"
+#DESTINATION = "../invoices/"
+
+CHUNK_SIZE =1048576 # = 2 ** 20 = 1MB
diff --git a/app/favicon.ico b/app/favicon.ico
new file mode 100644
index 0000000..1acc137
Binary files /dev/null and b/app/favicon.ico differ
diff --git a/app/favicon.svg b/app/favicon.svg
new file mode 100644
index 0000000..4fa96ce
--- /dev/null
+++ b/app/favicon.svg
@@ -0,0 +1,24 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/main.py b/app/main.py
new file mode 100644
index 0000000..2349f22
--- /dev/null
+++ b/app/main.py
@@ -0,0 +1,226 @@
+import os
+import io
+import fitz
+import qrcode
+import datetime
+import logging
+import urllib.parse
+from typing import Optional
+from dotenv import load_dotenv
+from fastapi.responses import FileResponse
+from fastapi import FastAPI, File, UploadFile, Response
+
+
+load_dotenv()
+SERVICE_URL = os.getenv('SERVICE_URL')
+DESTINATION = os.getenv('DESTINATION')
+CHUNK_SIZE = int(os.getenv('CHUNK_SIZE'))
+
+log = logging.getLogger(__name__)
+app = FastAPI()
+
+
+def urlencode(str):
+ return urllib.parse.quote(str)
+
+
+def urldecode(str):
+ return urllib.parse.unquote(str)
+
+def make_qrcode(qr_data):
+ """
+ Generate a QR code from the given qr_data.
+
+ Args:
+ qr_data (str): The data to be encoded in the QR code.
+
+ Returns:
+ io.BytesIO: The QR code image as a BytesIO object.
+ """
+ qr = qrcode.QRCode(
+ version=1,
+ error_correction=qrcode.constants.ERROR_CORRECT_L,
+ box_size=6,
+ border=2,
+ )
+ qr.add_data(qr_data)
+ qr.make(fit=True)
+
+ img = qr.make_image(fill_color="black", back_color="white")
+ fp = io.BytesIO()
+ img.save(fp, "PNG")
+ return fp
+
+def create_qr_rectangle(preset, page):
+ """
+ Create a QR rectangle based on the preset and page provided.
+
+ Parameters:
+ - preset (str): The preset for the QR rectangle.
+ - page (Page): The page on which the QR rectangle will be created.
+
+ Returns:
+ - rect (Rect): The created QR rectangle.
+ """
+ w = page.rect.width # page width
+
+ if preset == "inv1":
+ margin = 15
+ left = w*0.40
+ elif preset == "inv2":
+ margin = 20
+ left = w*0.88
+ else:
+ margin = 0
+ left = 0
+
+ rect = fitz.Rect(left, margin, left + 65, margin + 65)
+
+ return rect
+
+async def chunked_copy(src, dst):
+ """
+ Asynchronously copies the contents of the source file to the destination file
+ in chunks.
+
+ Args:
+ src: The source file object.
+ dst: The destination file path.
+
+ Returns:
+ None
+ """
+ await src.seek(0)
+ with open(dst, "wb") as buffer:
+ while True:
+ contents = await src.read(CHUNK_SIZE)
+ if not contents:
+ log.info(f"Src completely consumed\n")
+ break
+ log.info(f"Consumed {len(contents)} bytes from Src file\n")
+ buffer.write(contents)
+
+@app.post("/signinvoice/")
+async def sign_invoice(
+ file: UploadFile = File(...),
+ preset: Optional[str] = None,
+ code_content: Optional[str] = None,
+ company: Optional[str] = None,
+ inv_type: Optional[str] = None,
+ inv_series: Optional[str] = None,
+ inv_num: Optional[str] = None,
+ in_all: bool = False
+):
+ """
+ Sign an invoice file and generate a QR code for it.
+
+ Args:
+ file (UploadFile): The file to be signed.
+ preset (str, optional): The preset for the QR code.
+ code_content (str, optional): The content for the QR code.
+ company (str, optional): The company name.
+ inv_type (str, optional): The invoice type.
+ inv_series (str, optional): The invoice series.
+ inv_num (str, optional): The invoice number.
+ in_all (bool): Whether to include the QR code in all pages.
+
+ Returns:
+ FileResponse: The signed invoice file with QR code.
+ """
+
+ inv_code = "{}_{}_{}_{}".format(company, inv_type, inv_series, inv_num).replace(" ", "_").replace(",", "").replace("#", "")
+ inv_url = "{}/getinvoice/?inv_code={}".format(SERVICE_URL, inv_code)
+
+ #print("SIGNED URL: {}".format(inv_url))
+
+ temp_path = os.path.join(DESTINATION, f"{inv_code}_temp.pdf")
+ full_path = os.path.join(DESTINATION, f"{inv_code}.pdf")
+
+ await chunked_copy(file, temp_path)
+
+ if not code_content:
+ code_content = inv_url
+
+ qr_image = make_qrcode(code_content)
+ doc = fitz.open(temp_path)
+
+ add_qr = True
+ for page in doc:
+ if add_qr:
+ rect = create_qr_rectangle(preset, page)
+ if not page.is_wrapped:
+ page.wrap_contents()
+ page.insert_image(rect, stream=qr_image)
+ if not in_all and page.number == 0:
+ add_qr = False
+
+ doc.save(full_path, deflate=True, garbage=3)
+
+ if os.path.isfile(temp_path):
+ os.remove(temp_path)
+ print("{} : Returned Signed Invoice".format(datetime.datetime.now()))
+ return FileResponse(full_path)
+
+
+@app.get("/getinvoice/")
+async def get_invoice(
+ company: Optional[str] = None,
+ inv_code: Optional[str] = None,
+ inv_type: Optional[str] = None,
+ inv_series: Optional[str] = None,
+ inv_num: Optional[str] = None,
+ search_file: bool = False
+):
+ """
+ An asynchronous function to retrieve an invoice file based on various parameters.
+
+ Args:
+ company (Optional[str]): The company name.
+ inv_code (Optional[str]): The invoice code.
+ inv_type (Optional[str]): The invoice type.
+ inv_series (Optional[str]): The invoice series.
+ inv_num (Optional[str]): The invoice number.
+ search_file (bool): Flag to indicate whether to search for the file.
+
+ Returns:
+ Union[FileResponse, Dict[str, str]]: Returns the invoice file if found, otherwise returns a message.
+ """
+ if search_file:
+ inv_code = "{}_{}_{}_{}".format(company, inv_type, inv_series, inv_num).replace(" ", "_").replace(",", "").replace("#", "")
+
+ if inv_code:
+ full_path = os.path.join(DESTINATION, "{}.pdf".format(inv_code))
+ if os.path.isfile(full_path):
+ print("FETCHING INVOICE: {} | {}".format(inv_code, full_path))
+ return FileResponse(full_path)
+ else:
+ print("{} : No invoice found".format(datetime.datetime.now()))
+ return {"message": "No invoice found"}
+
+
+@app.get("/createqr/")
+async def create_qr_code(code_content: Optional[str] = None, token: Optional[str] = None):
+ """
+ Create a QR code from the given code_content.
+
+ Args:
+ code_content (str, optional): The content to be encoded in the QR code. Defaults to None.
+
+ Returns:
+ Response: The QR code image as a Response object with media type "image/png".
+ """
+ if token == 'ZPOclCF5od59SgW6PLM2':
+ if code_content:
+ qr_image = make_qrcode(code_content)
+ print("{} : QR Created".format(datetime.datetime.now()))
+ return Response(content=qr_image.getvalue(), media_type="image/png")
+ else:
+ print("{} : QR Code Content not provided".format(datetime.datetime.now()))
+ return "QR Code Content not provided"
+ else:
+ return "Unauthorized"
+
+
+@app.get('/favicon.ico', include_in_schema=False)
+async def favicon():
+ return FileResponse("favicon.ico")
diff --git a/docker-compose-local.yml b/docker-compose-local.yml
new file mode 100644
index 0000000..b2ea323
--- /dev/null
+++ b/docker-compose-local.yml
@@ -0,0 +1,15 @@
+version: '3.8'
+services:
+ qrcode-api:
+ container_name: qrcode-api
+ image: localhost:7776/qrcodes:latest
+ restart: always
+ volumes:
+ - ./invoices:/var/invoices
+ ports:
+ - 8188:80
+
+networks:
+ qrcode-net:
+ driver: bridge
+
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..a8a93bb
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,32 @@
+version: '3.5'
+services:
+ qrcode-api:
+ container_name: qrcode-api
+ image: git.argideli.com/quertex/qr-api:latest
+ restart: always
+ volumes:
+ - ./invoices:/var/invoices
+ networks:
+ - qrcode-net
+ ports:
+ - 8188:80
+ labels:
+ - "com.centurylinklabs.watchtower.scope=qrcode-api-master"
+
+ qrcode-api-watchtower:
+ container_name: qrcode-api-watchtower
+ image: containrrr/watchtower
+ networks:
+ - qrcode-net
+ volumes:
+ - /var/run/docker.sock:/var/run/docker.sock
+ - ./config.json:/config.json
+ command: --interval 300 --scope api
+ labels:
+ - "com.centurylinklabs.watchtower.scope=qrcode-api-master"
+
+networks:
+ qrcode-net:
+ name: qrcode-net
+ driver: bridge
+
diff --git a/invoices/.gitignore b/invoices/.gitignore
new file mode 100644
index 0000000..f08278d
--- /dev/null
+++ b/invoices/.gitignore
@@ -0,0 +1 @@
+*.pdf
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..5814fa7
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,18 @@
+annotated-types==0.6.0
+anyio==4.2.0
+click==8.1.7
+fastapi==0.109.0
+h11==0.14.0
+idna==3.6
+pydantic==2.5.3
+pydantic_core==2.14.6
+PyMuPDF==1.23.13
+PyMuPDFb==1.23.9
+pypng==0.20220715.0
+python-dotenv==1.0.0
+python-multipart==0.0.6
+qrcode==7.4.2
+sniffio==1.3.0
+starlette==0.35.1
+typing_extensions==4.9.0
+uvicorn==0.25.0
diff --git a/start_dev_server.sh b/start_dev_server.sh
new file mode 100644
index 0000000..46dfb5b
--- /dev/null
+++ b/start_dev_server.sh
@@ -0,0 +1,6 @@
+#/bin/bash
+source ./venv/bin/activate
+cd app
+uvicorn main:app --reload
+cd ..
+deactivate