diff --git a/omero_figure/scripts/omero/figure_scripts/Figure_To_Pdf.py b/omero_figure/scripts/omero/figure_scripts/Figure_To_Pdf.py index 9328b951d..207b41b2d 100644 --- a/omero_figure/scripts/omero/figure_scripts/Figure_To_Pdf.py +++ b/omero_figure/scripts/omero/figure_scripts/Figure_To_Pdf.py @@ -61,7 +61,7 @@ from reportlab.lib.colors import Color from reportlab.platypus import Paragraph from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_RIGHT - + from reportlab.lib.utils import ImageReader reportlab_installed = True except ImportError: reportlab_installed = False @@ -1677,6 +1677,222 @@ def draw_scalebar(self, panel, region_width, page): font_size, (red, green, blue), align="center") + def get_color_ramp(self, channel): + """ + Return the 256 1D array of the LUT from the server or + the color gradient. + + LUT files on the server are read with the script service, and + file content is parsed with a custom implementation. + """ + color = channel["color"] + + # Convert the hexadecimal string to RGB + color_ramp = None + if len(color) == 6: + try: + r = int(color[0:2], 16) + g = int(color[2:4], 16) + b = int(color[4:6], 16) + color_ramp = (numpy.linspace(0, 1, 256).reshape(-1, 1) + * numpy.array([r, g, b], dtype=numpy.uint8).T) + color_ramp = color_ramp.astype(numpy.uint8) + except ValueError: + pass + + else: + script_service = self.conn.getScriptService() + luts = script_service.getScriptsByMimetype("text/x-lut") + for lut in luts: + if lut.name.val != color: + continue + + orig_file = self.conn.getObject( + "OriginalFile", lut.getId()._val) + lut_data = bytearray() + # Collect the LUT data in byte form + for chunk in orig_file.getFileInChunks(): + lut_data.extend(chunk) + + if len(lut_data) in [768, 800]: + lut_arr = numpy.array(lut_data, dtype="uint8")[-768:] + color_ramp = lut_arr.reshape(3, 256).T + else: + lut_data = lut_data.decode() + r, g, b = [], [], [] + + lines = lut_data.split("\n") + sep = None + if "\t" in lines[0]: + sep = "\t" + for line in lines: + val = line.split(sep) + if len(val) < 3 or not val[-1].isnumeric(): + continue + r.append(int(val[-3])) + g.append(int(val[-2])) + b.append(int(val[-1])) + color_ramp = numpy.array([r, g, b], dtype=numpy.uint8).T + break + + if channel.get("reverseIntensity", False): + color_ramp = color_ramp[::-1] + + if color_ramp is None: + return numpy.zeros((1, 256, 3), dtype=numpy.uint8) + else: + return color_ramp[numpy.newaxis] + + def draw_colorbar(self, panel, page): + """ + Add the colorbar to the page. + Here we calculate the position of colorbar but delegate + to self.draw_scalebar_line() and self.draw_text() to actually place + the colorbar, ticks and labels on PDF/TIFF + """ + + colorbar = panel.get("colorbar", {}) + if not colorbar.get("show", False): + return + + channel = None + for c in panel["channels"]: + if c["active"]: + channel = c + break + if not channel: + return + + color_ramp = self.get_color_ramp(channel) + + spacing = colorbar["spacing"] + thickness = colorbar["thickness"] + cbar = { # Dict of colorbar properties to pass to paste_image + 'zoom': '100', + 'dx': 0, + 'dy': 0, + 'orig_height': panel['orig_height'], + 'orig_width': panel['orig_width'], + } + start, end = channel["window"]["start"], channel["window"]["end"] + + decimals = max(0, int(numpy.ceil( + -numpy.log10((end - start) / colorbar["num_ticks"])))) + labels = numpy.linspace(start, end, num=colorbar["num_ticks"]) + pos_ratio = numpy.linspace(0, 1, colorbar["num_ticks"]) + if colorbar["position"] in ["left", "right"]: + color_ramp = color_ramp.transpose((1, 0, 2))[::-1] + cbar['width'] = thickness + cbar['height'] = panel['height'] + cbar['y'] = panel['y'] + cbar['x'] = panel['x'] - (spacing + thickness) + labels_x = [cbar['x']] + labels_y = cbar['y'] + panel['height'] * pos_ratio[::-1] + align = "right" + if colorbar["position"] == "right": + cbar['x'] = panel['x'] + panel['width'] + spacing + labels_x = [cbar['x'] + cbar['width']] + align = "left" + labels_x *= labels.size # Duplicate x postions + elif colorbar["position"] in ["top", "bottom"]: + cbar['width'] = panel['width'] + cbar['height'] = thickness + cbar['x'] = panel['x'] + cbar['y'] = panel['y'] - (spacing + thickness) + labels_x = cbar['x'] + panel['width'] * pos_ratio + labels_y = [cbar['y']] + align = "center" + if colorbar["position"] == "bottom": + cbar['y'] = panel['y'] + panel['height'] + spacing + labels_y = [cbar['y'] + cbar['height']] + labels_y *= labels.size # Duplicate y postions + + pil_img = Image.fromarray(color_ramp) + img_name = channel["color"] + ".png" + + # for PDF export, we might have a target dpi + dpi = panel.get('min_export_dpi', None) + + # Paste the panel to PDF or TIFF image + self.paste_image(pil_img, img_name, cbar, page, dpi, + is_colorbar=True) + + rgb = tuple(int(colorbar["axis_color"][i:i+2], 16) for i in (0, 2, 4)) + fontsize = int(colorbar["font_size"]) + tick_width = 1 + tick_len = colorbar["tick_len"] + label_margin = colorbar["label_margin"] + contour_width = tick_width + for label, pos_x, pos_y in zip(labels, labels_x, labels_y): + + # Cosmetic correction, for first and last ticks to be + # aligned with the image + shift = 0 + if label == labels[0]: + shift = -tick_width / 2 + elif label == labels[-1]: + shift = tick_width / 2 + + # Round the label str to the appropriate decimal + label = f"{label:.{decimals}f}" + + if colorbar["position"] == "left": + x2 = pos_x - tick_len + pos_y += shift + self.draw_scalebar_line(pos_x, pos_y, x2, pos_y, + tick_width, rgb) + self.draw_text(label, pos_x - 4 - label_margin, + pos_y - fontsize / 2 + 1, + fontsize, rgb, align=align) + elif colorbar["position"] == "right": + x2 = pos_x + tick_len + pos_y += shift + self.draw_scalebar_line(pos_x, pos_y, x2, pos_y, + tick_width, rgb) + self.draw_text(label, pos_x + 4 + label_margin, + pos_y - fontsize / 2 + 1, + fontsize, rgb, align=align) + elif colorbar["position"] == "top": + y2 = pos_y - tick_len + pos_x -= shift # Order of the label is reversed + self.draw_scalebar_line(pos_x, pos_y, pos_x, y2, + tick_width, rgb) + self.draw_text(label, pos_x, + pos_y - fontsize - 2 - label_margin, + fontsize, rgb, align=align) + elif colorbar["position"] == "bottom": + y2 = pos_y + tick_len + pos_x -= shift # Order of the label is reversed + self.draw_scalebar_line(pos_x, pos_y, pos_x, y2, + tick_width, rgb) + self.draw_text(label, pos_x, pos_y + 4 + label_margin, + fontsize, rgb, align=align) + + if colorbar["position"] == "top": + self.draw_scalebar_line(cbar['x'], + cbar['y'], + cbar['x'] + cbar['width'], + cbar['y'], + contour_width, rgb) + elif colorbar["position"] == "bottom": + self.draw_scalebar_line(cbar['x'], + cbar['y'] + cbar['height'], + cbar['x'] + cbar['width'], + cbar['y'] + cbar['height'], + contour_width, rgb) + elif colorbar["position"] == "left": + self.draw_scalebar_line(cbar['x'], + cbar['y'], + cbar['x'], + cbar['y'] + cbar['height'], + contour_width, rgb) + elif colorbar["position"] == "right": + self.draw_scalebar_line(cbar['x'] + cbar['width'], + cbar['y'], + cbar['x'] + cbar['width'], + cbar['y'] + cbar['height'], + contour_width, rgb) + def is_big_image(self, image): """Return True if this is a 'big' tiled image.""" max_w, max_h = self.conn.getMaxPlaneSize() @@ -1936,12 +2152,6 @@ def draw_panel(self, panel, page, idx): """ image_id = panel['imageId'] channels = panel['channels'] - x = panel['x'] - y = panel['y'] - - # Handle page offsets - x = x - page['x'] - y = y - page['y'] image = self.conn.getObject("Image", image_id) if image is None: @@ -2150,6 +2360,7 @@ def add_panels_to_page(self, panels_json, image_ids, page): # Finally, add scale bar and labels to the page self.draw_scalebar(panel, pil_img.size[0], page) self.draw_labels(panel, page) + self.draw_colorbar(panel, page) def get_figure_file_ext(self): return "pdf" @@ -2266,7 +2477,8 @@ def draw_scalebar_line(self, x, y, x2, y2, width, rgb): c.setStrokeColorRGB(red, green, blue, 1) c.line(x, y, x2, y2) - def paste_image(self, pil_img, img_name, panel, page, dpi): + def paste_image(self, pil_img, img_name, panel, page, dpi, + is_colorbar=False): """ Adds the PIL image to the PDF figure. Overwritten for TIFFs """ # Apply flip transformations before drawing the image @@ -2307,8 +2519,15 @@ def paste_image(self, pil_img, img_name, panel, page, dpi): if self.zip_folder_name is not None: img_name = os.path.join(self.zip_folder_name, FINAL_DIR, img_name) - # Save Image to file, then bring into PDF - pil_img.save(img_name) + if is_colorbar: + # Save the image to a BytesIO stream + buffer = BytesIO() + pil_img.save(buffer, format="PNG") + buffer.seek(0) + img_name = ImageReader(buffer) # drawImage accepts ImageReader + else: + # Save Image to file, then bring into PDF + pil_img.save(img_name) # Since coordinate system is 'bottom-up', convert from 'top-down' y = self.page_height - height - y # set fill color alpha to fully opaque, since this impacts drawImage @@ -2374,7 +2593,8 @@ def add_page_color(self): """ Don't need to do anything for TIFF. Image is already colored.""" pass - def paste_image(self, pil_img, img_name, panel, page, dpi=None): + def paste_image(self, pil_img, img_name, panel, page, + dpi=None, is_colorbar=False): """ Add the PIL image to the current figure page """ # Apply flip transformations before drawing the image @@ -2405,8 +2625,9 @@ def paste_image(self, pil_img, img_name, panel, page, dpi=None): width = int(round(width)) height = int(round(height)) + export_img = self.export_images and not is_colorbar # Save image BEFORE resampling - if self.export_images: + if export_img: rs_name = os.path.join(self.zip_folder_name, RESAMPLED_DIR, img_name) pil_img.save(rs_name) @@ -2414,7 +2635,7 @@ def paste_image(self, pil_img, img_name, panel, page, dpi=None): # Resize to our target size to match DPI of figure pil_img = pil_img.resize((width, height), Image.BICUBIC) - if self.export_images: + if export_img: img_name = os.path.join(self.zip_folder_name, FINAL_DIR, img_name) pil_img.save(img_name) diff --git a/src/css/figure.css b/src/css/figure.css index 2cdc6a2c8..83d27d88a 100644 --- a/src/css/figure.css +++ b/src/css/figure.css @@ -1368,7 +1368,119 @@ color: white; border-color: #007bff; } - + .flipping .btn.active i { color: white; } + + .colorbar { + position: absolute; + width: 100%; + height: 100%; + } + + .colorbar_left, .colorbar_right{ + position: absolute; + top: 0%; + width: 100%; + height: 100%; + } + + .colorbar_left{ + right: 0%; + -webkit-transform: scaleX(-1) rotate(-90deg); + transform: scaleX(-1) rotate(-90deg); + } + + .colorbar_right{ + -webkit-transform: rotate(-90deg); + transform: rotate(-90deg); + } + + .colorbar_top, .colorbar_bottom { + position: absolute; + width: 100%; + left: 0%; + } + + .colorbar_top { + bottom: 0%; + } + + .colorbar_bottom { + top: 0%; + } + + .colorbar_bg { + background-size: 100% var(--pngHeight); + background-repeat: no-repeat; + margin-right: 0px; + margin-left: 0px; + display: flex; + position: relative; + } + + .colorbar_ticks_right, .colorbar_ticks_left { + height: 100%; + display: flex; + flex-direction: column-reverse; + justify-content: space-between; + position: absolute; + } + + .colorbar_ticks_right { + align-items: left; + } + + .colorbar_ticks_left { + right: 0%; + align-items: flex-end; + } + + .colorbar_ticks_top, .colorbar_ticks_bottom { + width: 100%; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: flex-end; + position: absolute; + } + + .colorbar_ticks_top { + bottom: 0%; + } + + .colorbar_ticks_bottom { + top: 0%; + } + + .colorbar_label_horizontal { + line-height: 1; + } + + .colorbar_label_vertical { + height: 0px; + line-height: 1px; + } + + .colorbar_dash_horizontal { + width: 1px; + position: relative; + } + + .colorbar_dash_vertical { + height: 1px; + position: relative; + } + + .colorbar_tick_horizontal { + display: flex; + flex-direction: column; + align-items: center; + width: 1px; + position: relative; + } + + .colorbar_tick_vertical { + display: flex; + } diff --git a/src/index.html b/src/index.html index 1d528a75f..374c19b18 100644 --- a/src/index.html +++ b/src/index.html @@ -1871,6 +1871,10 @@
Scalebar

+
Colorbar
+
+
+
Add Labels
+ + +
+ <% if (show){ %> + + <% } else { %> + + <% } %> +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ +
+
+