Last active
January 2, 2016 17:09
-
-
Save peterjc/8334631 to your computer and use it in GitHub Desktop.
Standalone test case originally Biopython for resting ReportLab under Python 2 and 3. Currently seeing a segmentation fault under Python 2.7, and a TypeError under Python 3.3 when testing on Mac OS X - see http://two.pairlist.net/pipermail/reportlab-users/2014-January/010972.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from reportlab.pdfgen import canvas | |
from reportlab.lib.pagesizes import letter | |
from reportlab.lib.units import inch | |
from reportlab.lib import colors | |
from reportlab.pdfbase.pdfmetrics import stringWidth | |
from reportlab.graphics.shapes import Drawing, String, Line, Rect, Wedge, ArcPath | |
from reportlab.graphics import renderPDF, renderPS | |
from reportlab.graphics.widgetbase import Widget | |
# The following code is to allow all the Bio.Graphics | |
# code to deal with the different ReportLab renderers | |
# and the API quirks consistently. | |
def _write(drawing, output_file, format, dpi=72): | |
"""Helper function to standardize output to files (PRIVATE). | |
Writes the provided drawing out to a file in a prescribed format. | |
drawing - suitable ReportLab drawing object. | |
output_file - a handle to write to, or a filename to write to. | |
format - String indicating output format, one of PS, PDF, SVG, | |
or provided the ReportLab renderPM module is installed, | |
one of the bitmap formats JPG, BMP, GIF, PNG, TIFF or TIFF. | |
The format can be given in any case. | |
dpi - Resolution (dots per inch) for bitmap formats. | |
No return value. | |
""" | |
from reportlab.graphics import renderPS, renderPDF, renderSVG | |
try: | |
from reportlab.graphics import renderPM | |
except ImportError: | |
#This is an optional part of ReportLab, so may not be installed. | |
#We'll raise a missing dependency error if rendering to a | |
#bitmap format is attempted. | |
renderPM=None | |
formatdict = {'PS': renderPS, 'EPS': renderPS, | |
# not sure which you actually get, PS or EPS, but | |
# GenomeDiagram used PS while other modules used EPS. | |
'PDF': renderPDF, | |
'SVG': renderSVG, | |
'JPG': renderPM, | |
'BMP': renderPM, | |
'GIF': renderPM, | |
'PNG': renderPM, | |
'TIFF': renderPM, | |
'TIF': renderPM | |
} | |
try: | |
#If output is not a string, then .upper() will trigger | |
#an attribute error... | |
drawmethod = formatdict[format.upper()] # select drawing method | |
except (KeyError, AttributeError): | |
raise ValueError("Output format should be one of %s" | |
% ", ".join(formatdict)) | |
if drawmethod is None: | |
#i.e. We wanted renderPM but it isn't installed | |
#See the import at the top of the function. | |
from Bio import MissingPythonDependencyError | |
raise MissingPythonDependencyError( | |
"Please install ReportLab's renderPM module") | |
if drawmethod == renderPM: | |
#This has a different API to the other render objects | |
return drawmethod.drawToFile(drawing, output_file, | |
format, dpi=dpi) | |
else: | |
return drawmethod.drawToFile(drawing, output_file) | |
class _ChromosomeComponent(Widget): | |
"""Base class specifying the interface for a component of the system. | |
This class should not be instantiated directly, but should be used | |
from derived classes. | |
""" | |
def __init__(self): | |
"""Initialize a chromosome component. | |
Attributes: | |
o _sub_components -- Any components which are contained under | |
this parent component. This attribute should be accessed through | |
the add() and remove() functions. | |
""" | |
self._sub_components = [] | |
def add(self, component): | |
"""Add a sub_component to the list of components under this item. | |
""" | |
assert isinstance(component, _ChromosomeComponent), \ | |
"Expected a _ChromosomeComponent object, got %s" % component | |
self._sub_components.append(component) | |
def remove(self, component): | |
"""Remove the specified component from the subcomponents. | |
Raises a ValueError if the component is not registered as a | |
sub_component. | |
""" | |
try: | |
self._sub_components.remove(component) | |
except ValueError: | |
raise ValueError("Component %s not found in sub_components." % | |
component) | |
def draw(self): | |
"""Draw the specified component. | |
""" | |
raise AssertionError("Subclasses must implement.") | |
class Organism(_ChromosomeComponent): | |
"""Top level class for drawing chromosomes. | |
This class holds information about an organism and all of it's | |
chromosomes, and provides the top level object which could be used | |
for drawing a chromosome representation of an organism. | |
Chromosomes should be added and removed from the Organism via the | |
add and remove functions. | |
""" | |
def __init__(self, output_format = 'pdf'): | |
_ChromosomeComponent.__init__(self) | |
# customizable attributes | |
self.page_size = letter | |
self.title_size = 20 | |
#Do we need this given we don't draw a legend? | |
#If so, should be a public API... | |
self._legend_height = 0 # 2 * inch | |
self.output_format = output_format | |
def draw(self, output_file, title): | |
"""Draw out the information for the Organism. | |
Arguments: | |
o output_file -- The name of a file specifying where the | |
document should be saved, or a handle to be written to. | |
The output format is set when creating the Organism object. | |
Alternatively, output_file=None will return the drawing using | |
the low-level ReportLab objects (for further processing, such | |
as adding additional graphics, before writing). | |
o title -- The output title of the produced document. | |
""" | |
width, height = self.page_size | |
cur_drawing = Drawing(width, height) | |
self._draw_title(cur_drawing, title, width, height) | |
cur_x_pos = inch * .5 | |
if len(self._sub_components) > 0: | |
x_pos_change = (width - inch) / len(self._sub_components) | |
# no sub_components | |
else: | |
pass | |
for sub_component in self._sub_components: | |
# set the drawing location of the chromosome | |
sub_component.start_x_position = cur_x_pos + 0.05 * x_pos_change | |
sub_component.end_x_position = cur_x_pos + 0.95 * x_pos_change | |
sub_component.start_y_position = height - 1.5 * inch | |
sub_component.end_y_position = self._legend_height + 1 * inch | |
# do the drawing | |
sub_component.draw(cur_drawing) | |
# update the locations for the next chromosome | |
cur_x_pos += x_pos_change | |
self._draw_legend(cur_drawing, self._legend_height + 0.5 * inch, width) | |
if output_file is None: | |
#Let the user take care of writing to the file... | |
return cur_drawing | |
return _write(cur_drawing, output_file, self.output_format) | |
def _draw_title(self, cur_drawing, title, width, height): | |
"""Write out the title of the organism figure. | |
""" | |
title_string = String(width / 2, height - inch, title) | |
title_string.fontName = 'Helvetica-Bold' | |
title_string.fontSize = self.title_size | |
title_string.textAnchor = "middle" | |
cur_drawing.add(title_string) | |
def _draw_legend(self, cur_drawing, start_y, width): | |
"""Draw a legend for the figure. | |
Subclasses should implement this (see also self._legend_height) to | |
provide specialized legends. | |
""" | |
pass | |
class Chromosome(_ChromosomeComponent): | |
"""Class for drawing a chromosome of an organism. | |
This organizes the drawing of a single organisms chromosome. This | |
class can be instantiated directly, but the draw method makes the | |
most sense to be called in the context of an organism. | |
""" | |
def __init__(self, chromosome_name): | |
"""Initialize a Chromosome for drawing. | |
Arguments: | |
o chromosome_name - The label for the chromosome. | |
Attributes: | |
o start_x_position, end_x_position - The x positions on the page | |
where the chromosome should be drawn. This allows multiple | |
chromosomes to be drawn on a single page. | |
o start_y_position, end_y_position - The y positions on the page | |
where the chromosome should be contained. | |
Configuration Attributes: | |
o title_size - The size of the chromosome title. | |
o scale_num - A number of scale the drawing by. This is useful if | |
you want to draw multiple chromosomes of different sizes at the | |
same scale. If this is not set, then the chromosome drawing will | |
be scaled by the number of segements in the chromosome (so each | |
chromosome will be the exact same final size). | |
""" | |
_ChromosomeComponent.__init__(self) | |
self._name = chromosome_name | |
self.start_x_position = -1 | |
self.end_x_position = -1 | |
self.start_y_position = -1 | |
self.end_y_position = -1 | |
self.title_size = 20 | |
self.scale_num = None | |
self.label_size = 6 | |
self.chr_percent = 0.25 | |
self.label_sep_percent = self.chr_percent * 0.5 | |
self._color_labels = False | |
def subcomponent_size(self): | |
"""Return the scaled size of all subcomponents of this component. | |
""" | |
total_sub = 0 | |
for sub_component in self._sub_components: | |
total_sub += sub_component.scale | |
return total_sub | |
def draw(self, cur_drawing): | |
"""Draw a chromosome on the specified template. | |
Ideally, the x_position and y_*_position attributes should be | |
set prior to drawing -- otherwise we're going to have some problems. | |
""" | |
for position in (self.start_x_position, self.end_x_position, | |
self.start_y_position, self.end_y_position): | |
assert position != -1, "Need to set drawing coordinates." | |
# first draw all of the sub-sections of the chromosome -- this | |
# will actually be the picture of the chromosome | |
cur_y_pos = self.start_y_position | |
if self.scale_num: | |
y_pos_change = ((self.start_y_position * .95 - self.end_y_position) | |
/ self.scale_num) | |
elif len(self._sub_components) > 0: | |
y_pos_change = ((self.start_y_position * .95 - self.end_y_position) | |
/ self.subcomponent_size()) | |
# no sub_components to draw | |
else: | |
pass | |
left_labels = [] | |
right_labels = [] | |
for sub_component in self._sub_components: | |
this_y_pos_change = sub_component.scale * y_pos_change | |
# set the location of the component to draw | |
sub_component.start_x_position = self.start_x_position | |
sub_component.end_x_position = self.end_x_position | |
sub_component.start_y_position = cur_y_pos | |
sub_component.end_y_position = cur_y_pos - this_y_pos_change | |
# draw the sub component | |
sub_component._left_labels = [] | |
sub_component._right_labels = [] | |
sub_component.draw(cur_drawing) | |
left_labels += sub_component._left_labels | |
right_labels += sub_component._right_labels | |
# update the position for the next component | |
cur_y_pos -= this_y_pos_change | |
self._draw_labels(cur_drawing, left_labels, right_labels) | |
self._draw_label(cur_drawing, self._name) | |
def _draw_label(self, cur_drawing, label_name): | |
"""Draw a label for the chromosome. | |
""" | |
x_position = 0.5 * (self.start_x_position + self.end_x_position) | |
y_position = self.end_y_position | |
label_string = String(x_position, y_position, label_name) | |
label_string.fontName = 'Times-BoldItalic' | |
label_string.fontSize = self.title_size | |
label_string.textAnchor = 'middle' | |
cur_drawing.add(label_string) | |
def _draw_labels(self, cur_drawing, left_labels, right_labels): | |
"""Layout and draw sub-feature labels for the chromosome.""" | |
if not self._sub_components: | |
return | |
color_label = self._color_labels | |
segment_width = (self.end_x_position - self.start_x_position) \ | |
* self.chr_percent | |
label_sep = (self.end_x_position - self.start_x_position) \ | |
* self.label_sep_percent | |
segment_x = self.start_x_position \ | |
+ 0.5 * (self.end_x_position - self.start_x_position - segment_width) | |
y_limits = [] | |
for sub_component in self._sub_components: | |
y_limits.extend((sub_component.start_y_position, sub_component.end_y_position)) | |
y_min = min(y_limits) | |
y_max = max(y_limits) | |
del y_limits | |
#Now do some label placement magic... | |
#from reportlab.pdfbase import pdfmetrics | |
#font = pdfmetrics.getFont('Helvetica') | |
#h = (font.face.ascent + font.face.descent) * 0.90 | |
h = self.label_size | |
for x1, x2, labels, anchor in [ | |
(segment_x, | |
segment_x - label_sep, | |
_place_labels(left_labels, y_min, y_max, h), | |
"end"), | |
(segment_x + segment_width, | |
segment_x + segment_width + label_sep, | |
_place_labels(right_labels, y_min, y_max, h), | |
"start"), | |
]: | |
for (y1, y2, color, back_color, name) in labels: | |
cur_drawing.add(Line(x1, y1, x2, y2, | |
strokeColor = color, | |
strokeWidth = 0.25)) | |
label_string = String(x2, y2, name, | |
textAnchor=anchor) | |
label_string.fontName = 'Helvetica' | |
label_string.fontSize = h | |
if color_label: | |
label_string.fillColor = color | |
if back_color: | |
w = stringWidth(name, label_string.fontName, label_string.fontSize) | |
if x1 > x2: | |
w = w * -1.0 | |
cur_drawing.add(Rect(x2, y2 - 0.1*h, w, h, | |
strokeColor=back_color, | |
fillColor=back_color)) | |
cur_drawing.add(label_string) | |
class ChromosomeSegment(_ChromosomeComponent): | |
"""Draw a segment of a chromosome. | |
This class provides the important configurable functionality of drawing | |
a Chromosome. Each segment has some customization available here, or can | |
be subclassed to define additional functionality. Most of the interesting | |
drawing stuff is likely to happen at the ChromosomeSegment level. | |
""" | |
def __init__(self): | |
"""Initialize a ChromosomeSegment. | |
Attributes: | |
o start_x_position, end_x_position - Defines the x range we have | |
to draw things in. | |
o start_y_position, end_y_position - Defines the y range we have | |
to draw things in. | |
Configuration Attributes: | |
o scale - A scaling value for the component. By default this is | |
set at 1 (ie -- has the same scale as everything else). Higher | |
values give more size to the component, smaller values give less. | |
o fill_color - A color to fill in the segment with. Colors are | |
available in reportlab.lib.colors | |
o label - A label to place on the chromosome segment. This should | |
be a text string specifying what is to be included in the label. | |
o label_size - The size of the label. | |
o chr_percent - The percentage of area that the chromosome | |
segment takes up. | |
""" | |
_ChromosomeComponent.__init__(self) | |
self.start_x_position = -1 | |
self.end_x_position = -1 | |
self.start_y_position = -1 | |
self.end_y_position = -1 | |
# --- attributes for configuration | |
self.scale = 1 | |
self.fill_color = None | |
self.label = None | |
self.label_size = 6 | |
self.chr_percent = .25 | |
def draw(self, cur_drawing): | |
"""Draw a chromosome segment. | |
Before drawing, the range we are drawing in needs to be set. | |
""" | |
for position in (self.start_x_position, self.end_x_position, | |
self.start_y_position, self.end_y_position): | |
assert position != -1, "Need to set drawing coordinates." | |
self._draw_subcomponents(cur_drawing) # Anything behind | |
self._draw_segment(cur_drawing) | |
self._overdraw_subcomponents(cur_drawing) # Anything on top | |
self._draw_label(cur_drawing) | |
def _draw_subcomponents(self, cur_drawing): | |
"""Draw any subcomponents of the chromosome segment. | |
This should be overridden in derived classes if there are | |
subcomponents to be drawn. | |
""" | |
pass | |
def _draw_segment(self, cur_drawing): | |
"""Draw the current chromosome segment. | |
""" | |
# set the coordinates of the segment -- it'll take up the MIDDLE part | |
# of the space we have. | |
segment_y = self.end_y_position | |
segment_width = (self.end_x_position - self.start_x_position) \ | |
* self.chr_percent | |
segment_height = self.start_y_position - self.end_y_position | |
segment_x = self.start_x_position \ | |
+ 0.5 * (self.end_x_position - self.start_x_position - segment_width) | |
# first draw the sides of the segment | |
right_line = Line(segment_x, segment_y, | |
segment_x, segment_y + segment_height) | |
left_line = Line(segment_x + segment_width, segment_y, | |
segment_x + segment_width, segment_y + segment_height) | |
cur_drawing.add(right_line) | |
cur_drawing.add(left_line) | |
# now draw the box, if it is filled in | |
if self.fill_color is not None: | |
fill_rectangle = Rect(segment_x, segment_y, | |
segment_width, segment_height) | |
fill_rectangle.fillColor = self.fill_color | |
fill_rectangle.strokeColor = None | |
cur_drawing.add(fill_rectangle) | |
def _overdraw_subcomponents(self, cur_drawing): | |
"""Draw any subcomponents of the chromosome segment over the main part. | |
This should be overridden in derived classes if there are | |
subcomponents to be drawn. | |
""" | |
pass | |
def _draw_label(self, cur_drawing): | |
"""Add a label to the chromosome segment. | |
The label will be applied to the right of the segment. | |
This may be overlapped by any sub-feature labels on other segments! | |
""" | |
if self.label is not None: | |
label_x = 0.5 * (self.start_x_position + self.end_x_position) + \ | |
(self.chr_percent + 0.05) * (self.end_x_position - | |
self.start_x_position) | |
label_y = ((self.start_y_position - self.end_y_position) / 2 + | |
self.end_y_position) | |
label_string = String(label_x, label_y, self.label) | |
label_string.fontName = 'Helvetica' | |
label_string.fontSize = self.label_size | |
cur_drawing.add(label_string) | |
def _place_labels(desired_etc, minimum, maximum, gap=0): | |
#Simified implemetation: | |
return sorted(desired_etc) | |
# hold the chromosome info for testing | |
# the info is structured as (label, color, scale) | |
chr1_info = (("", None, 1), | |
("AC30001", None, 1), | |
("", colors.blue, 2), | |
("", colors.blue, 1.5), | |
("", colors.red, 2), | |
("AC12345", colors.blue, 1), | |
("", colors.blue, 1), | |
("", colors.blue, 1), | |
("", None, 1)) | |
def load_chromosome(chr_name, chr_segment_info): | |
"""Load a chromosome and all of its segments.""" | |
cur_chromosome = Chromosome(chr_name) | |
for label, fill_color, scale in chr_segment_info: | |
cur_segment = ChromosomeSegment() | |
if label != "": | |
cur_segment.label = label | |
if fill_color is not None: | |
cur_segment.fill_color = fill_color | |
cur_segment.scale = scale | |
cur_chromosome.add(cur_segment) | |
return cur_chromosome | |
def simple_organism(filename, format): | |
"""Output a simple organism to given format.""" | |
test_organism = Organism(format) | |
test_organism.add(load_chromosome("I", chr1_info)) | |
test_organism.draw(filename, "Test organism") | |
simple_organism("organism.eps", "ps") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment