Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

QR image #49

Merged
merged 4 commits into from
Jul 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file not shown.
98 changes: 53 additions & 45 deletions src/hope_country_report/apps/power_query/processors.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Any, Dict, List, Optional, TYPE_CHECKING
from typing import Any, Dict, List, Optional, Tuple, TYPE_CHECKING

import io
import logging
Expand All @@ -22,18 +22,16 @@
from strategy_field.utils import fqn

from hope_country_report.apps.power_query.storage import HopeStorage

from hope_country_report.apps.power_query.utils import (
get_field_rect,
to_dataset,
convert_pdf_to_image_pdf,
get_field_rect,
insert_qr_code,
insert_special_image,
to_dataset,
)

logger = logging.getLogger(__name__)
if TYPE_CHECKING:
from typing import Tuple

from .models import Dataset, Formatter, ReportTemplate

ProcessorResult = bytes | BytesIO
Expand Down Expand Up @@ -189,71 +187,85 @@ def process(self, context: "Dict[str, Any]") -> "ProcessorResult":


class ToFormPDF(ProcessorStrategy):
"""
Produce reports in PDF form or cards.
This class handles PDF generation with special attention to QR code fields.
"""

file_suffix = ".pdf"
format = TYPE_DETAIL
needs_file = True

def process(self, context: Dict[str, Any]) -> bytes:
tpl = self.formatter.template
reader = PdfReader(tpl.doc)

font_size = context.get("context", {}).get("font_size", 10)
font_color = context.get("context", {}).get("font_color", "black")
ds = to_dataset(context["dataset"].data).dict
output_pdf = PdfWriter()

for index, entry in enumerate(ds, start=1):
with NamedTemporaryFile(suffix=".pdf", delete=True) as temp_pdf_file:
writer = PdfWriter()
text_values = {}
special_values = {}
images = {}
try:
for page in reader.pages:
for annot in page.annotations:
annot = annot.get_object()
field_name = annot[FieldDictionaryAttributes.T]
if field_name in entry:
value = entry[field_name]
language = self.is_special_language_field(field_name)
if self.is_image_field(annot):
rect = annot[AnnotationDictionaryAttributes.Rect]
text_values[field_name] = None
images[field_name] = [rect, value]
elif language:
special_values[field_name] = {"value": value, "language": language}
else:
text_values[field_name] = value
except IndexError as exc:
capture_exception(exc)
logger.exception(exc)
raise
qr_codes = {}

for page in reader.pages:
for annot in page.annotations:
annot = annot.get_object()
field_name = annot[FieldDictionaryAttributes.T]
if field_name in entry:
value = entry[field_name]
language = self.is_special_language_field(field_name)
if field_name.endswith("_qr"):
qr_codes[field_name] = value
elif self.is_image_field(annot):
rect = annot[AnnotationDictionaryAttributes.Rect]
images[field_name] = (rect, value)
elif language:
special_values[field_name] = {"value": value, "language": language}
else:
text_values[field_name] = value

writer.append(reader)
writer.update_page_form_field_values(writer.pages[-1], text_values, flags=FieldFlag.READ_ONLY)
output_stream = io.BytesIO()
writer.write(output_stream)
output_stream.seek(0)
temp_pdf_file.write(output_stream.read())

# Open processed document for image and QR code insertion
document = fitz.open(stream=output_stream.getvalue(), filetype="pdf")
for field_name, text in special_values.items():
insert_special_image(document, field_name, text, font_size, font_color)
for field_name, (rect, image_path) in images.items():
if image_path:
self.insert_external_image(document, field_name, image_path)
else:
logger.warning(f"Image not found for field: {field_name}")
document.ez_save(temp_pdf_file.name, deflate_fonts=True, deflate_images=1, deflate=1)
self.insert_images_and_qr_codes(document, images, qr_codes, special_values, font_size, font_color)
document.save(temp_pdf_file.name)
output_stream.seek(0)
output_pdf.append_pages_from_reader(PdfReader(temp_pdf_file.name))

output_stream = io.BytesIO()
output_pdf.write(output_stream)
output_stream.seek(0)
fitz_pdf_document = fitz.open(stream=output_stream, filetype="pdf")

# Convert the PDF to an image-based PDF
image_pdf_bytes = convert_pdf_to_image_pdf(fitz_pdf_document, dpi=300)

return image_pdf_bytes
fitz_pdf_document = fitz.open("pdf", output_stream.read())
return convert_pdf_to_image_pdf(fitz_pdf_document, dpi=300)

def insert_images_and_qr_codes(
self,
document: fitz.Document,
images: Dict[str, Tuple[fitz.Rect, str]],
qr_codes: Dict[str, str],
special_values: Dict[str, Dict[str, str]],
font_size: int,
font_color: str,
):
for field_name, text in special_values.items():
insert_special_image(document, field_name, text, int(font_size), font_color)
for field_name, (rect, image_path) in images.items():
self.insert_external_image(document, field_name, image_path, rect)
for field_name, data in qr_codes.items():
rect, page_index = get_field_rect(document, field_name)
insert_qr_code(document, field_name, data, rect, page_index)

def insert_external_image(self, document: fitz.Document, field_name: str, image_path: str, font_size: int = 10):
"""
Expand Down Expand Up @@ -295,11 +307,7 @@ def is_image_field(self, annot: ArrayObject) -> bool:
"""
Checks if a given PDF annotation represents an image field.
"""
return (
annot.get(FieldDictionaryAttributes.FT) == "/Btn"
and AnnotationDictionaryAttributes.P in annot
and AnnotationDictionaryAttributes.AP in annot
)
return annot.get(FieldDictionaryAttributes.FT) == "/Btn" and AnnotationDictionaryAttributes.AP in annot

def is_special_language_field(self, field_name: str) -> Optional[str]:
"""Extract language code from the field name if it exists."""
Expand Down
45 changes: 39 additions & 6 deletions src/hope_country_report/apps/power_query/utils.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,28 @@
from typing import Any, Dict, Optional, TYPE_CHECKING
from pathlib import Path

from io import BytesIO
from django.conf import settings
import base64
import binascii
import datetime
import hashlib
import io
import json
import logging
from collections.abc import Callable, Iterable
from functools import wraps
from PIL import Image, ImageDraw, ImageFont
from pathlib import Path

from django.conf import settings
from django.contrib.auth import authenticate
from django.db.models import QuerySet
from django.http import HttpRequest, HttpResponse
from django.utils.safestring import mark_safe

import fitz
import qrcode
import tablib
from constance import config
from PIL import Image, ImageDraw, ImageFont
from sentry_sdk import configure_scope

if TYPE_CHECKING:
Expand Down Expand Up @@ -214,10 +218,11 @@ def convert_pdf_to_image_pdf(pdf_document: fitz.Document, dpi: int = 300) -> byt
"""
new_pdf_document = fitz.open()

for page_num in range(len(pdf_document)):
for page_num in range(pdf_document.page_count): # Use .page_count here
pix = pdf_document[page_num].get_pixmap(dpi=dpi)
new_pdf_document.new_page(width=pix.width, height=pix.height)
new_pdf_document[page_num].insert_image(fitz.Rect(0, 0, pix.width, pix.height), pixmap=pix)
new_page = new_pdf_document.new_page(width=pix.width, height=pix.height)
new_page.insert_image(fitz.Rect(0, 0, pix.width, pix.height), pixmap=pix)

new_pdf_bytes = BytesIO()
new_pdf_document.save(new_pdf_bytes, deflate_fonts=1, deflate_images=1, deflate=1)
new_pdf_bytes.seek(0)
Expand All @@ -241,3 +246,31 @@ def insert_special_image(
page.insert_image(img_rect, stream=image_stream, keep_proportion=False)
else:
logger.info(f"Field {field_name} not found")


def insert_qr_code(document: fitz.Document, field_name: str, data: str, rect: fitz.Rect, page_index: int):
"""
Generates a QR code and inserts it into the specified field.
"""
qr = qrcode.QRCode(
version=1,
error_correction=qrcode.constants.ERROR_CORRECT_L,
box_size=10,
border=2,
)
qr.add_data(data)
qr.make(fit=True)

qr_image = qr.make_image(fill_color="black", back_color="white")
image_stream = io.BytesIO()
qr_image.save(image_stream, format="PNG")
image_stream.seek(0)

page = document[page_index]

for widget in page.widgets():
if widget.field_name == field_name:
page.delete_widget(widget)
break

page.insert_image(rect, stream=image_stream, keep_proportion=False)
2 changes: 2 additions & 0 deletions tests/extras/testutils/factories/power_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
from django.apps import apps
from django.core.files.base import ContentFile
from django.core.files.uploadedfile import SimpleUploadedFile

import factory
from strategy_field.utils import fqn

from hope_country_report.apps.power_query.models import (
ChartPage,
Dataset,
Expand Down
32 changes: 25 additions & 7 deletions tests/power_query/test_pq_utils.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,33 @@
import datetime
from base64 import b64encode
from io import BytesIO
from pathlib import Path
import pytest
from unittest.mock import MagicMock
from hope_country_report.utils.media import resource_path

from django.contrib.auth.models import AnonymousUser
from django.http import HttpResponse
from django.utils import timezone

import fitz
from PIL import Image
from io import BytesIO
import tablib
from PIL import Image
from pytz import utc

from hope_country_report.apps.power_query.utils import (
basicauth,
convert_pdf_to_image_pdf,
get_field_rect,
get_sentry_url,
insert_qr_code,
insert_special_image,
insert_special_language_image,
is_valid_template,
sentry_tags,
sizeof,
to_dataset,
insert_special_image,
insert_special_language_image,
convert_pdf_to_image_pdf,
get_field_rect,
)
from hope_country_report.utils.media import resource_path

TEST_PDF = resource_path("apps/power_query/doc_templates/program_receipt.pdf")

Expand Down Expand Up @@ -221,3 +224,18 @@ def test_insert_special_image(sample_pdf: fitz.Document) -> None:

annotations = [annot for annot in page.annots()]
assert len(annotations) == 0, "No annotations found after insertion"


def test_insert_qr_code(sample_pdf):
field_name = "code_qr"
data = "https://example.com"
page = sample_pdf[0]
rect, page_index = get_field_rect(sample_pdf, field_name)

if rect is not None and page_index is not None:
insert_qr_code(sample_pdf, field_name, data, rect, page_index)
page = sample_pdf[page_index]
images = page.get_images(full=True)
assert len(images) > 0, "No images found on the page, QR code insertion failed"
else:
pytest.fail("No valid rectangle or page index found for the field.")
Loading