feat: barcode printing
This commit is contained in:
BIN
assets/fonts/Arial Nova Cond.ttf
Normal file
BIN
assets/fonts/Arial Nova Cond.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/DejaVuSans.ttf
Normal file
BIN
assets/fonts/DejaVuSans.ttf
Normal file
Binary file not shown.
2
modules/fulfillment_base/barcodes_pdf_gen/__init__.py
Normal file
2
modules/fulfillment_base/barcodes_pdf_gen/__init__.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
from .barcode_pdf_generator import BarcodePdfGenerator as BarcodePdfGenerator
|
||||||
|
from .types import BarcodeData as BarcodeData
|
||||||
@ -0,0 +1,161 @@
|
|||||||
|
from io import BytesIO
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
from reportlab.graphics.barcode import code128
|
||||||
|
from reportlab.lib.units import mm
|
||||||
|
from reportlab.platypus import Spacer, PageBreak, Paragraph
|
||||||
|
|
||||||
|
from modules.fulfillment_base.barcodes_pdf_gen.types import *
|
||||||
|
from utils.pdf import PdfMaker, PDFGenerator
|
||||||
|
|
||||||
|
|
||||||
|
class BarcodePdfGenerator(PDFGenerator):
|
||||||
|
def _get_attr_by_path(
|
||||||
|
self, value: Any, path: str
|
||||||
|
) -> Optional[str | int | float | bool]:
|
||||||
|
keys = path.split(".")
|
||||||
|
for key in keys:
|
||||||
|
try:
|
||||||
|
if isinstance(value, dict):
|
||||||
|
value = value[key]
|
||||||
|
else:
|
||||||
|
value = getattr(value, key)
|
||||||
|
except (KeyError, AttributeError, TypeError):
|
||||||
|
return None
|
||||||
|
return value
|
||||||
|
|
||||||
|
def generate(
|
||||||
|
self, barcodes_data: list[BarcodeData | PdfBarcodeImageGenData]
|
||||||
|
) -> BytesIO:
|
||||||
|
pdf_barcodes_gen_data: list[PdfBarcodeGenData | PdfBarcodeImageGenData] = []
|
||||||
|
|
||||||
|
for barcode_data in barcodes_data:
|
||||||
|
if "barcode" not in barcode_data:
|
||||||
|
pdf_barcodes_gen_data.append(barcode_data)
|
||||||
|
continue
|
||||||
|
|
||||||
|
attributes = {}
|
||||||
|
for attribute in barcode_data["template"].attributes:
|
||||||
|
value = self._get_attr_by_path(barcode_data["product"], attribute.key)
|
||||||
|
if not value or not value.strip():
|
||||||
|
continue
|
||||||
|
attributes[attribute.name] = value
|
||||||
|
barcode_text = "<br/>".join(
|
||||||
|
[f"{key}: {value}" for key, value in attributes.items()]
|
||||||
|
)
|
||||||
|
|
||||||
|
pdf_barcodes_gen_data.append(
|
||||||
|
{
|
||||||
|
"barcode_value": barcode_data["barcode"],
|
||||||
|
"text": barcode_text,
|
||||||
|
"num_duplicates": barcode_data["num_duplicates"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return self._generate(pdf_barcodes_gen_data)
|
||||||
|
|
||||||
|
def _generate(
|
||||||
|
self, barcodes_data: list[PdfBarcodeGenData | PdfBarcodeImageGenData]
|
||||||
|
) -> BytesIO:
|
||||||
|
pdf_maker = PdfMaker((self.page_width, self.page_height))
|
||||||
|
|
||||||
|
pdf_files: list[BytesIO] = []
|
||||||
|
|
||||||
|
for barcode_data in barcodes_data:
|
||||||
|
if "barcode_value" in barcode_data:
|
||||||
|
pdf_files.append(self._generate_for_one_product(barcode_data))
|
||||||
|
else:
|
||||||
|
pdf_files.append(self._generate_for_one_product_using_img(barcode_data))
|
||||||
|
pdf_files.append(self._generate_spacers())
|
||||||
|
|
||||||
|
for file in pdf_files[:-1]:
|
||||||
|
pdf_maker.add_pdfs(file)
|
||||||
|
|
||||||
|
return pdf_maker.get_bytes()
|
||||||
|
|
||||||
|
def _generate_for_one_product(self, barcode_data: PdfBarcodeGenData) -> BytesIO:
|
||||||
|
buffer = BytesIO()
|
||||||
|
doc = self._create_doc(buffer)
|
||||||
|
|
||||||
|
# Создаем абзац с новым стилем
|
||||||
|
paragraph = Paragraph(barcode_data["text"], self.small_style)
|
||||||
|
|
||||||
|
# Получаем ширину и высоту абзаца
|
||||||
|
paragraph_width, paragraph_height = paragraph.wrap(
|
||||||
|
self.page_width - 2 * mm, self.page_height
|
||||||
|
)
|
||||||
|
|
||||||
|
# Рассчитываем доступное пространство для штрихкода
|
||||||
|
human_readable_height = 6 * mm # Высота human-readable текста
|
||||||
|
space_between_text_and_barcode = 4 * mm # Отступ между текстом и штрихкодом
|
||||||
|
barcode_height = (
|
||||||
|
self.page_height
|
||||||
|
- paragraph_height
|
||||||
|
- human_readable_height
|
||||||
|
- space_between_text_and_barcode
|
||||||
|
- 4 * mm
|
||||||
|
) # Учитываем поля и отступы
|
||||||
|
|
||||||
|
# Создаем штрихкод
|
||||||
|
available_width = self.page_width - 4 * mm # Учитываем поля
|
||||||
|
|
||||||
|
# Приблизительное количество элементов в штрихкоде Code 128 для средней длины
|
||||||
|
num_elements = 11 * len(
|
||||||
|
barcode_data["barcode_value"]
|
||||||
|
) # Примерная оценка: 11 элементов на символ
|
||||||
|
|
||||||
|
# Рассчитываем ширину штриха
|
||||||
|
bar_width = available_width / num_elements
|
||||||
|
barcode = code128.Code128(
|
||||||
|
barcode_data["barcode_value"],
|
||||||
|
barWidth=bar_width,
|
||||||
|
barHeight=barcode_height,
|
||||||
|
humanReadable=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Добавление штрихкодов в список элементов документа
|
||||||
|
elements = []
|
||||||
|
for _ in range(barcode_data["num_duplicates"]):
|
||||||
|
elements.append(paragraph)
|
||||||
|
elements.append(
|
||||||
|
Spacer(1, space_between_text_and_barcode)
|
||||||
|
) # Отступ между текстом и штрихкодом
|
||||||
|
elements.append(PageBreak())
|
||||||
|
|
||||||
|
# Функция для отрисовки штрихкода на canvas
|
||||||
|
def add_barcode(canvas, doc):
|
||||||
|
barcode_width = barcode.width
|
||||||
|
barcode_x = (self.page_width - barcode_width) / 2 # Центрируем штрихкод
|
||||||
|
# Размещаем штрихкод снизу с учетом отступа
|
||||||
|
barcode_y = human_readable_height + 2 * mm
|
||||||
|
barcode.drawOn(canvas, barcode_x, barcode_y)
|
||||||
|
|
||||||
|
# Создаем документ
|
||||||
|
doc.build(
|
||||||
|
elements, onFirstPage=add_barcode, onLaterPages=add_barcode
|
||||||
|
) # Убираем последний PageBreak
|
||||||
|
|
||||||
|
buffer.seek(0)
|
||||||
|
return buffer
|
||||||
|
|
||||||
|
def _generate_for_one_product_using_img(
|
||||||
|
self, barcode_data: PdfBarcodeImageGenData
|
||||||
|
) -> BytesIO:
|
||||||
|
with open(barcode_data["barcode_image_url"], "rb") as pdf_file:
|
||||||
|
pdf_bytes = pdf_file.read()
|
||||||
|
|
||||||
|
pdf_maker = PdfMaker((self.page_width, self.page_height))
|
||||||
|
for _ in range(barcode_data["num_duplicates"]):
|
||||||
|
pdf_maker.add_pdfs(BytesIO(pdf_bytes))
|
||||||
|
|
||||||
|
return pdf_maker.get_bytes()
|
||||||
|
|
||||||
|
def _generate_spacers(self) -> BytesIO:
|
||||||
|
buffer = BytesIO()
|
||||||
|
doc = self._create_doc(buffer)
|
||||||
|
elements = []
|
||||||
|
for _ in range(self.number_of_spacing_pages):
|
||||||
|
elements.append(PageBreak())
|
||||||
|
doc.build(elements)
|
||||||
|
buffer.seek(0)
|
||||||
|
return buffer
|
||||||
21
modules/fulfillment_base/barcodes_pdf_gen/types.py
Normal file
21
modules/fulfillment_base/barcodes_pdf_gen/types.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
from typing import TypedDict
|
||||||
|
|
||||||
|
from modules.fulfillment_base.models import BarcodeTemplate, Product
|
||||||
|
|
||||||
|
|
||||||
|
class BarcodeData(TypedDict):
|
||||||
|
barcode: str
|
||||||
|
template: BarcodeTemplate
|
||||||
|
product: Product
|
||||||
|
num_duplicates: int
|
||||||
|
|
||||||
|
|
||||||
|
class PdfBarcodeGenData(TypedDict):
|
||||||
|
barcode_value: str
|
||||||
|
text: str
|
||||||
|
num_duplicates: int
|
||||||
|
|
||||||
|
|
||||||
|
class PdfBarcodeImageGenData(TypedDict):
|
||||||
|
barcode_image_url: str
|
||||||
|
num_duplicates: int
|
||||||
@ -32,6 +32,12 @@ class BarcodeTemplateRepository(
|
|||||||
.order_by(BarcodeTemplate.id)
|
.order_by(BarcodeTemplate.id)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _process_get_by_id_stmt(self, stmt: Select) -> Select:
|
||||||
|
return stmt.options(
|
||||||
|
selectinload(BarcodeTemplate.attributes),
|
||||||
|
joinedload(BarcodeTemplate.size),
|
||||||
|
)
|
||||||
|
|
||||||
async def _get_size_by_id(self, size_id: int) -> Optional[BarcodeTemplateSize]:
|
async def _get_size_by_id(self, size_id: int) -> Optional[BarcodeTemplateSize]:
|
||||||
stmt = select(BarcodeTemplateSize).where(BarcodeTemplateSize.id == size_id)
|
stmt = select(BarcodeTemplateSize).where(BarcodeTemplateSize.id == size_id)
|
||||||
result = await self.session.scalars(stmt)
|
result = await self.session.scalars(stmt)
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
from sqlalchemy import or_, delete
|
from sqlalchemy import or_, delete
|
||||||
from sqlalchemy.orm import selectinload
|
from sqlalchemy.orm import selectinload, joinedload
|
||||||
|
|
||||||
from modules.fulfillment_base.models import Product, ProductBarcode
|
from modules.fulfillment_base.models import Product, ProductBarcode, BarcodeTemplate
|
||||||
from modules.fulfillment_base.schemas.product import (
|
from modules.fulfillment_base.schemas.product import (
|
||||||
CreateProductSchema,
|
CreateProductSchema,
|
||||||
UpdateProductSchema,
|
UpdateProductSchema,
|
||||||
@ -55,7 +55,13 @@ class ProductRepository(
|
|||||||
return list(result.scalars().all()), total_items
|
return list(result.scalars().all()), total_items
|
||||||
|
|
||||||
def _process_get_by_id_stmt(self, stmt: Select) -> Select:
|
def _process_get_by_id_stmt(self, stmt: Select) -> Select:
|
||||||
return stmt.options(selectinload(Product.barcodes))
|
return stmt.options(
|
||||||
|
selectinload(Product.barcodes),
|
||||||
|
joinedload(Product.client),
|
||||||
|
joinedload(Product.barcode_template).selectinload(
|
||||||
|
BarcodeTemplate.attributes
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
async def _after_create(self, product: Product, data: CreateProductSchema) -> None:
|
async def _after_create(self, product: Product, data: CreateProductSchema) -> None:
|
||||||
new_barcodes = [
|
new_barcodes = [
|
||||||
|
|||||||
@ -2,7 +2,7 @@ from fastapi import APIRouter, Path, Query
|
|||||||
|
|
||||||
from backend.dependecies import SessionDependency, PaginationDependency
|
from backend.dependecies import SessionDependency, PaginationDependency
|
||||||
from modules.fulfillment_base.schemas.product import *
|
from modules.fulfillment_base.schemas.product import *
|
||||||
from modules.fulfillment_base.services import ProductService
|
from modules.fulfillment_base.services import ProductService, BarcodePrinterService
|
||||||
|
|
||||||
router = APIRouter(tags=["product"])
|
router = APIRouter(tags=["product"])
|
||||||
|
|
||||||
@ -56,3 +56,18 @@ async def delete_product(
|
|||||||
pk: int = Path(),
|
pk: int = Path(),
|
||||||
):
|
):
|
||||||
return await ProductService(session).delete(pk)
|
return await ProductService(session).delete(pk)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/barcode/get-pdf",
|
||||||
|
operation_id="get_product_barcode_pdf",
|
||||||
|
response_model=GetProductBarcodePdfResponse,
|
||||||
|
)
|
||||||
|
async def get_product_barcode_pdf(
|
||||||
|
request: GetProductBarcodePdfRequest, session: SessionDependency
|
||||||
|
):
|
||||||
|
service = BarcodePrinterService(session)
|
||||||
|
filename, base64_string = await service.generate_base64(request)
|
||||||
|
return GetProductBarcodePdfResponse(
|
||||||
|
base64_string=base64_string, filename=filename, mime_type="application/pdf"
|
||||||
|
)
|
||||||
|
|||||||
@ -4,7 +4,7 @@ from pydantic import field_validator
|
|||||||
|
|
||||||
from modules.fulfillment_base.models import ProductBarcode
|
from modules.fulfillment_base.models import ProductBarcode
|
||||||
from modules.fulfillment_base.schemas.barcode_template import BarcodeTemplateSchema
|
from modules.fulfillment_base.schemas.barcode_template import BarcodeTemplateSchema
|
||||||
from schemas.base import BaseSchema, BaseResponse, PaginationInfoSchema
|
from schemas.base import BaseSchema, BaseResponse, PaginationInfoSchema, BasePdfResponse
|
||||||
|
|
||||||
|
|
||||||
# region Entity
|
# region Entity
|
||||||
@ -69,6 +69,12 @@ class UpdateProductRequest(BaseSchema):
|
|||||||
entity: UpdateProductSchema
|
entity: UpdateProductSchema
|
||||||
|
|
||||||
|
|
||||||
|
class GetProductBarcodePdfRequest(BaseSchema):
|
||||||
|
quantity: int
|
||||||
|
product_id: int
|
||||||
|
barcode: str
|
||||||
|
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
# region Response
|
# region Response
|
||||||
@ -91,4 +97,8 @@ class DeleteProductResponse(BaseResponse):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class GetProductBarcodePdfResponse(BasePdfResponse):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|||||||
@ -6,3 +6,4 @@ from .service import ServiceModelService as ServiceModelService
|
|||||||
from .services_kit import ServicesKitService as ServicesKitService
|
from .services_kit import ServicesKitService as ServicesKitService
|
||||||
from .service_category import ServiceCategoryService as ServiceCategoryService
|
from .service_category import ServiceCategoryService as ServiceCategoryService
|
||||||
from .barcode_template import BarcodeTemplateService as BarcodeTemplateService
|
from .barcode_template import BarcodeTemplateService as BarcodeTemplateService
|
||||||
|
from .barcode_printer_service import BarcodePrinterService as BarcodePrinterService
|
||||||
|
|||||||
41
modules/fulfillment_base/services/barcode_printer_service.py
Normal file
41
modules/fulfillment_base/services/barcode_printer_service.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import base64
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from modules.fulfillment_base.barcodes_pdf_gen import BarcodePdfGenerator, BarcodeData
|
||||||
|
from modules.fulfillment_base.models import Product
|
||||||
|
from modules.fulfillment_base.repositories import ProductRepository
|
||||||
|
from modules.fulfillment_base.schemas.product import GetProductBarcodePdfRequest
|
||||||
|
|
||||||
|
|
||||||
|
class BarcodePrinterService:
|
||||||
|
session: AsyncSession
|
||||||
|
|
||||||
|
def __init__(self, session: AsyncSession):
|
||||||
|
self.session = session
|
||||||
|
|
||||||
|
async def generate_pdf(
|
||||||
|
self, request: GetProductBarcodePdfRequest
|
||||||
|
) -> tuple[str, BytesIO]:
|
||||||
|
product: Product = await ProductRepository(self.session).get_by_id(
|
||||||
|
request.product_id
|
||||||
|
)
|
||||||
|
barcode_data: BarcodeData = {
|
||||||
|
"barcode": request.barcode,
|
||||||
|
"template": product.barcode_template,
|
||||||
|
"product": product,
|
||||||
|
"num_duplicates": request.quantity,
|
||||||
|
}
|
||||||
|
filename = f"{product.id}_barcode.pdf"
|
||||||
|
|
||||||
|
size = product.barcode_template.size
|
||||||
|
generator = BarcodePdfGenerator(size.width, size.height)
|
||||||
|
return filename, generator.generate([barcode_data])
|
||||||
|
|
||||||
|
async def generate_base64(
|
||||||
|
self, request: GetProductBarcodePdfRequest
|
||||||
|
) -> tuple[str, str]:
|
||||||
|
filename, pdf_buffer = await self.generate_pdf(request)
|
||||||
|
base64_string = base64.b64encode(pdf_buffer.read()).decode("utf-8")
|
||||||
|
return filename, base64_string
|
||||||
11
requirements.txt
Normal file
11
requirements.txt
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
PyMuPDF
|
||||||
|
pdfrw
|
||||||
|
fpdf
|
||||||
|
reportlab
|
||||||
|
fastapi
|
||||||
|
SQLAlchemy
|
||||||
|
pathlib
|
||||||
|
python-dotenv
|
||||||
|
starlette
|
||||||
|
pydantic
|
||||||
|
alembic
|
||||||
@ -61,3 +61,9 @@ class BaseEnumSchema(BaseSchema):
|
|||||||
|
|
||||||
class BaseEnumListSchema(BaseSchema):
|
class BaseEnumListSchema(BaseSchema):
|
||||||
items: list[BaseEnumSchema]
|
items: list[BaseEnumSchema]
|
||||||
|
|
||||||
|
|
||||||
|
class BasePdfResponse(BaseSchema):
|
||||||
|
base64_string: str
|
||||||
|
filename: str
|
||||||
|
mime_type: str
|
||||||
|
|||||||
2
utils/pdf/__init__.py
Normal file
2
utils/pdf/__init__.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
from .pdf_maker import PdfMaker as PdfMaker
|
||||||
|
from .pdf_generator import PDFGenerator as PDFGenerator
|
||||||
45
utils/pdf/pdf_generator.py
Normal file
45
utils/pdf/pdf_generator.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from reportlab.lib.pagesizes import mm
|
||||||
|
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
||||||
|
from reportlab.pdfbase import pdfmetrics
|
||||||
|
from reportlab.pdfbase.ttfonts import TTFont
|
||||||
|
from reportlab.platypus import SimpleDocTemplate
|
||||||
|
|
||||||
|
from constants import APP_PATH
|
||||||
|
|
||||||
|
|
||||||
|
class PDFGenerator:
|
||||||
|
def __init__(self, page_width: int, page_height: int):
|
||||||
|
ASSETS_FOLDER = os.path.join(APP_PATH, "assets")
|
||||||
|
FONTS_FOLDER = os.path.join(ASSETS_FOLDER, "fonts")
|
||||||
|
FONT_FILE_PATH = os.path.join(FONTS_FOLDER, "DejaVuSans.ttf")
|
||||||
|
self.page_width = page_width * mm
|
||||||
|
self.page_height = page_height * mm
|
||||||
|
self.number_of_spacing_pages = 1
|
||||||
|
|
||||||
|
pdfmetrics.registerFont(TTFont("DejaVuSans", FONT_FILE_PATH))
|
||||||
|
|
||||||
|
# Get standard styles and create a new style with a smaller font size
|
||||||
|
styles = getSampleStyleSheet()
|
||||||
|
self.small_style = ParagraphStyle(
|
||||||
|
"Small",
|
||||||
|
parent=styles["Normal"],
|
||||||
|
fontName="DejaVuSans", # Specify the new font
|
||||||
|
fontSize=6,
|
||||||
|
leading=7,
|
||||||
|
spaceAfter=2,
|
||||||
|
leftIndent=2,
|
||||||
|
rightIndent=2,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Создание документа с указанным размером страницы
|
||||||
|
def _create_doc(self, buffer):
|
||||||
|
return SimpleDocTemplate(
|
||||||
|
buffer,
|
||||||
|
pagesize=(self.page_width, self.page_height),
|
||||||
|
rightMargin=1 * mm,
|
||||||
|
leftMargin=1 * mm,
|
||||||
|
topMargin=1 * mm,
|
||||||
|
bottomMargin=1 * mm,
|
||||||
|
)
|
||||||
81
utils/pdf/pdf_maker.py
Normal file
81
utils/pdf/pdf_maker.py
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
import fitz
|
||||||
|
import pdfrw
|
||||||
|
from fpdf import FPDF
|
||||||
|
|
||||||
|
|
||||||
|
class PdfMaker:
|
||||||
|
def __init__(self, size: tuple):
|
||||||
|
self.size = size
|
||||||
|
|
||||||
|
self.writer = pdfrw.PdfWriter()
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
del self.writer
|
||||||
|
self.writer = pdfrw.PdfWriter()
|
||||||
|
|
||||||
|
def add_image(self, image_data):
|
||||||
|
fpdf = FPDF(format=self.size, unit="pt")
|
||||||
|
width, height = self.size
|
||||||
|
fpdf.add_page()
|
||||||
|
fpdf.image(image_data, 0, 0, width, height)
|
||||||
|
fpdf_reader: pdfrw.PdfReader = pdfrw.PdfReader(fdata=bytes(fpdf.output()))
|
||||||
|
self.writer.addpage(fpdf_reader.getPage(0))
|
||||||
|
|
||||||
|
def add_pdf(self, pdf_data: BytesIO):
|
||||||
|
pdf_reader = pdfrw.PdfReader(fdata=bytes(pdf_data.read()))
|
||||||
|
self.writer.addpage(pdf_reader.getPage(0))
|
||||||
|
|
||||||
|
def add_pdfs(self, pdf_data: BytesIO):
|
||||||
|
pdf_reader = pdfrw.PdfReader(fdata=bytes(pdf_data.read()))
|
||||||
|
self.writer.addpages(pdf_reader.readpages(pdf_reader.Root))
|
||||||
|
|
||||||
|
def get_bytes(self):
|
||||||
|
result_io = BytesIO()
|
||||||
|
self.writer.write(result_io)
|
||||||
|
result_io.seek(0)
|
||||||
|
return result_io
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_target_rect(page: fitz.Page, target_ratio: float) -> fitz.Rect:
|
||||||
|
original_width, original_height = page.rect.width, page.rect.height
|
||||||
|
|
||||||
|
if original_width / original_height > target_ratio:
|
||||||
|
# Image is wider than target aspect ratio
|
||||||
|
new_width = original_width
|
||||||
|
new_height = int(original_width / target_ratio)
|
||||||
|
else:
|
||||||
|
# Image is taller than target aspect ratio
|
||||||
|
new_height = original_height
|
||||||
|
new_width = int(new_height * target_ratio)
|
||||||
|
|
||||||
|
return fitz.Rect(0, 0, new_width, new_height)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def resize_pdf_with_reportlab(input_pdf_bytesio: BytesIO) -> BytesIO:
|
||||||
|
output_pdf = BytesIO()
|
||||||
|
|
||||||
|
pdf_document = fitz.open(stream=input_pdf_bytesio.getvalue(), filetype="pdf")
|
||||||
|
|
||||||
|
if len(pdf_document) != 1:
|
||||||
|
raise Exception("Ошибка. В документе должна быть одна страница.")
|
||||||
|
|
||||||
|
page = pdf_document[0]
|
||||||
|
target_ratio = 29 / 20
|
||||||
|
actual_ratio = page.rect.width / page.rect.height
|
||||||
|
|
||||||
|
if abs(actual_ratio - target_ratio) < 0.1:
|
||||||
|
return input_pdf_bytesio
|
||||||
|
|
||||||
|
rect = PdfMaker._get_target_rect(page, target_ratio)
|
||||||
|
page.set_mediabox(rect)
|
||||||
|
page.set_cropbox(rect)
|
||||||
|
page.set_bleedbox(rect)
|
||||||
|
page.set_trimbox(rect)
|
||||||
|
|
||||||
|
pdf_document.save(output_pdf)
|
||||||
|
pdf_document.close()
|
||||||
|
|
||||||
|
output_pdf.seek(0)
|
||||||
|
return output_pdf
|
||||||
Reference in New Issue
Block a user