You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
205 lines
6.7 KiB
205 lines
6.7 KiB
import io |
|
import PIL |
|
|
|
from collections import namedtuple |
|
from itertools import zip_longest |
|
from reportlab.pdfgen.canvas import Canvas |
|
from reportlab.lib.pagesizes import A4 |
|
from reportlab.lib.styles import getSampleStyleSheet |
|
from reportlab.lib.units import inch, cm |
|
from reportlab.platypus import ( |
|
Flowable, |
|
Image, |
|
KeepTogether, |
|
PageBreak, |
|
Paragraph, |
|
Spacer, |
|
SimpleDocTemplate, |
|
Table, |
|
) |
|
|
|
ImageBuffer = namedtuple("ImageBuffer", ["buffer", "width", "height"]) |
|
FailedDropImage = namedtuple("FailedDropImage", ["path", "well"]) |
|
|
|
styles = getSampleStyleSheet() |
|
style_n = styles["Normal"] |
|
style_h1 = styles["Heading1"] |
|
style_h2 = styles["Heading2"] |
|
|
|
TABLE_STYLE = [ |
|
("TOPPADDING", (0, 0), (-1, -1), 0), |
|
("RIGHTPADDING", (0, 0), (-1, -1), 7), |
|
("BOTTOMPADDING", (0, 0), (-1, -1), 0), |
|
("LEFTPADDING", (0, 0), (-1, -1), 0), |
|
("FONTSIZE", (0, 0), (-1, -1), 8), |
|
] |
|
|
|
|
|
class DropPictures(Flowable): |
|
"""A row of drop pictures flowable.""" |
|
|
|
def __init__(self, pictures, xoffset=0): |
|
self.pictures = filter(None, pictures) |
|
self.xoffset = xoffset |
|
self.size = 3.75 * cm |
|
self.offsets = [0 * cm, 6 * cm, 12 * cm] |
|
|
|
def wrap(self, *args): |
|
return (self.xoffset, self.size + 1 * cm) |
|
|
|
def draw(self): |
|
canvas = self.canv |
|
for offset, picture in zip(self.offsets, self.pictures): |
|
canvas.drawImage(picture.path, offset, 0, width=5 * cm, height=self.size) |
|
canvas.drawString(offset + 0.5 * cm, 3.0 * cm, picture.well) |
|
|
|
|
|
|
|
def print_info_flowable(data): |
|
version = data.print.software_version |
|
content = [ |
|
("Printer:", data.print.printer), |
|
("Software version:", f"{version.major}.{version.minor}.{version.patch}"), |
|
( |
|
"Humidity Setting:", |
|
f"{data.print.humidity_setting} (humidifier might be turned off)", |
|
), |
|
("Run Method:", data.print.run_method), |
|
("Source Plate:", data.print.source_plate), |
|
("Print Solutions:", f"{data.print.print_solutions} solutions"), |
|
("Target Substrate:", data.print.target_substrate), |
|
("Number of Targets:", f"{data.print.target_count} targets printed"), |
|
] |
|
if data.print.pattern_file: |
|
content.append(("Pattern File:", data.print.pattern_file)) |
|
nozzles = sorted(data.statistics.nozzles) |
|
content.append(("Number of Nozzles:", len(nozzles))) |
|
for nozzle in nozzles: |
|
content.append( |
|
( |
|
f"Settings Nozzle #{nozzle.number}:", |
|
f"{nozzle.voltage}V, {nozzle.pulse}µs", |
|
) |
|
) |
|
content.append(("Failed Drop Checks, Pre Run:", data.statistics.failed_pre_run)) |
|
content.append(("Failed Drop Checks, Post Run:", data.statistics.failed_post_run)) |
|
|
|
return Table(content, style=TABLE_STYLE, hAlign="LEFT") |
|
|
|
|
|
def trim_image(image_path): |
|
original = PIL.Image.open(image_path) |
|
background = PIL.Image.new(original.mode, original.size, original.getpixel((0, 0))) |
|
diff = PIL.ImageChops.difference(original, background) |
|
diff = PIL.ImageChops.add(diff, diff, 2.0, -100) |
|
left, upper, right, lower = diff.getbbox() |
|
bbox = (left - 10, upper - 10, right + 12, lower + 10) |
|
cropped = original.crop(bbox) if bbox else original |
|
buffer = io.BytesIO() |
|
cropped.save(buffer, format="png") |
|
return ImageBuffer(buffer, cropped.width, cropped.height) |
|
|
|
|
|
def scaled_image_flowable(image_path, width=17 * cm): |
|
image_buffer = trim_image(image_path) |
|
height = (width / image_buffer.width) * image_buffer.height |
|
return Image(image_buffer.buffer, width=width, height=height) |
|
|
|
|
|
def graph_flowable(title, file_path): |
|
section = [ |
|
Paragraph(title, style_h2), |
|
Spacer(width=17 * cm, height=0.5 * cm), |
|
scaled_image_flowable(file_path), |
|
] |
|
return KeepTogether(section) |
|
|
|
def get_failed_drop_images(failed_checks): |
|
return [ |
|
FailedDropImage(item.path.with_suffix(".jpg"), item.well) |
|
for item in failed_checks.itertuples() |
|
] |
|
|
|
def failed_drops_flowable(nozzle, measurement): |
|
if measurement == "Pre Run": |
|
failed_checks = nozzle.drops_failed.pre_run |
|
elif measurement == "Post Run": |
|
failed_checks = nozzle.drops_failed.post_run |
|
else: |
|
raise ValueError(f"Unknown mesurement: {measurement}") |
|
failed_images = get_failed_drop_images(failed_checks) |
|
|
|
if len(failed_images) == 0: |
|
# no images to display here, we return early |
|
return [] |
|
|
|
section = [ |
|
PageBreak(), |
|
Paragraph(f"Failed Drop Images: Nozzle #{nozzle.number}, {measurement}", style_h2) |
|
] |
|
|
|
# group three images together |
|
failed_iterator = iter(failed_images) |
|
failed_groups = zip_longest(failed_iterator, failed_iterator, failed_iterator) |
|
|
|
for group in failed_groups: |
|
section.append(DropPictures(group)) |
|
|
|
return section |
|
|
|
|
|
def generate_report(data, graphs): |
|
|
|
story = [] |
|
|
|
start = data.print.environment.index.min() |
|
start_str = start.strftime("%Y-%m-%d %H:%m") |
|
end = start = data.print.environment.index.max() |
|
end_str = end.strftime("%Y-%m-%d %H:%m") |
|
headline = Paragraph(f"Print {start_str} - {end_str}", style_h1) |
|
story.append(headline) |
|
story.append(Spacer(width=17 * cm, height=0.5 * cm)) |
|
|
|
story.append(print_info_flowable(data)) |
|
story.append(Spacer(width=17 * cm, height=0.5 * cm)) |
|
|
|
story.append(graph_flowable("Environment Graphs", graphs.environment)) |
|
|
|
for nozzle in sorted(data.statistics.nozzles): |
|
story.append(PageBreak()) |
|
|
|
path = graphs.drops[nozzle.number] |
|
story.append( |
|
graph_flowable(f"Drop Check Graphs, Nozzle #{nozzle.number}", path) |
|
) |
|
|
|
story.append(Spacer(width=17 * cm, height=0.5 * cm)) |
|
|
|
if len(nozzle.drops_failed.pre_run) == 0: |
|
failed_wells_pre_run = "-" |
|
else: |
|
failed_wells_pre_run = ", ".join(nozzle.drops_failed.pre_run["well"]) |
|
if len(nozzle.drops_failed.post_run) == 0: |
|
failed_wells_post_run = "-" |
|
else: |
|
failed_wells_post_run = ", ".join(nozzle.drops_failed.post_run["well"]) |
|
content = [ |
|
("Failed Pre Run Checks:", failed_wells_pre_run), |
|
("Failed Post Run Checks:", failed_wells_post_run), |
|
] |
|
story.append(Table(content, style=TABLE_STYLE, hAlign="LEFT")) |
|
story.extend(failed_drops_flowable(nozzle, "Pre Run")) |
|
story.extend(failed_drops_flowable(nozzle, "Post Run")) |
|
|
|
pdf_path = data.files.folder / f"{data.files.folder.name}_report.pdf" |
|
doc = SimpleDocTemplate( |
|
str(pdf_path), |
|
pagesize=A4, |
|
leftMargin=2 * cm, |
|
rightMargin=2 * cm, |
|
topMargin=2 * cm, |
|
bottomMargin=2 * cm, |
|
) |
|
doc.build(story) |
|
|
|
return pdf_path
|
|
|