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
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