Skip to content

Commit 6bb878e

Browse files
author
mbonnefoy
committed
Refactor printing module - step 6
Make mapnik.printing a package Clean exceptions handling and add logging Use context managers instead of files creation/deletions Document magic numbers Rename arguments and add calls via keyword args
1 parent f29a472 commit 6bb878e

4 files changed

Lines changed: 76 additions & 82 deletions

File tree

Lines changed: 76 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,19 @@
44

55
from __future__ import absolute_import, print_function
66

7+
import logging
78
import math
8-
import os
9-
import tempfile
109

1110
import mapnik
1211
from mapnik import Box2d, Coord, Geometry, Layer, Map, Projection, Style, render
13-
from mapnik.conversions import m2pt, m2px
14-
from mapnik.formats import pagesizes
15-
from mapnik.scales import any_scale, default_scale, deg_min_sec_scale, sequence_scale
12+
from mapnik.printing.conversions import m2pt, m2px
13+
from mapnik.printing.formats import pagesizes
14+
from mapnik.printing.scales import any_scale, default_scale, deg_min_sec_scale, sequence_scale
1615

1716
try:
1817
import cairo
19-
HAS_PYCAIRO_MODULE = True
2018
except ImportError:
21-
HAS_PYCAIRO_MODULE = False
19+
raise ImportError("Could not import pycairo; PDF rendering only available when pycairo is available")
2220

2321
try:
2422
import pangocairo
@@ -137,10 +135,6 @@ def __init__(self,
137135
self._box = Box2d(percent_box[0] * pagesize[0], percent_box[1] * pagesize[1],
138136
percent_box[2] * pagesize[0], percent_box[3] * pagesize[1])
139137

140-
if not HAS_PYCAIRO_MODULE:
141-
raise Exception(
142-
"PDF rendering only available when pycairo is available")
143-
144138
self.font_name = "DejaVu Sans"
145139

146140
def render_map(self, m, filename):
@@ -407,7 +401,7 @@ def _render_scale_axes(self, first, first_percent, page_div_size, div_size, is_x
407401
value = first_percent * (end - start) + start
408402

409403
while value < end:
410-
self._draw_line(ctx, m2pt(value), m2pt(boundary_start), m2pt(value), m2pt(boundary_end))
404+
self._draw_line(ctx, m2pt(value), m2pt(boundary_start), m2pt(value), m2pt(boundary_end), line_width=0.5)
411405
self._render_scale_boxes(ctx, boundary_start, boundary_end, prev, value, text=text, fill_color=fill_color)
412406

413407
prev = value
@@ -421,7 +415,7 @@ def _render_scale_axes(self, first, first_percent, page_div_size, div_size, is_x
421415
# ensure that the last box gets drawn
422416
self._render_scale_boxes(ctx, boundary_start, boundary_end, prev, end, fill_color=fill_color)
423417

424-
def _draw_line(self, ctx, start_x, start_y, end_x, end_y, stroke_color=(0.5, 0.5, 0.5), line_width=1):
418+
def _draw_line(self, ctx, start_x, start_y, end_x, end_y, line_width=1, stroke_color=(0.5, 0.5, 0.5)):
425419
"""Draws a line from (start_x, start_y) to (end_x, end_y) on the specified cairo context."""
426420
ctx.save()
427421

@@ -456,23 +450,23 @@ def _render_box(self, ctx, rectangle, text=None, stroke_color=(0.0, 0.0, 0.0), f
456450

457451
if text:
458452
ctx.move_to(rectangle.x + 1, rectangle.y)
459-
self.write_text(ctx, text, fill_color=[1 - z for z in fill_color], size=rectangle.height - 2)
453+
self.write_text(ctx, text, size=rectangle.height - 2, stroke_color=[1 - z for z in fill_color])
460454

461455
ctx.restore()
462456

463-
def write_text(self, ctx, text, box_width=None, size=10, fill_color=(0.0, 0.0, 0.0), alignment=None):
457+
def write_text(self, ctx, text, box_width=None, size=10, stroke_color=(0.0, 0.0, 0.0), alignment=None):
464458
"""
465459
Writes the text to the specified context.
466460
467461
Returns:
468462
A rectangle (x, y, width, height) representing the extents of the text drawn
469463
"""
470464
if HAS_PANGOCAIRO_MODULE:
471-
return self._write_text_pangocairo(ctx, text, box_width, size, fill_color, alignment)
465+
return self._write_text_pangocairo(ctx, text, box_width=box_width, size=size, stroke_color=stroke_color, alignment=alignment)
472466
else:
473-
return self._write_text_cairo(ctx, text, box_width, size)
467+
return self._write_text_cairo(ctx, text, size=size, stroke_color=stroke_color)
474468

475-
def _write_text_pangocairo(self, ctx, text, box_width=None, size=10, fill_color=(0.0, 0.0, 0.0), alignment=None):
469+
def _write_text_pangocairo(self, ctx, text, box_width=None, size=10, stroke_color=(0.0, 0.0, 0.0), alignment=None):
476470
"""
477471
Use a pango.Layout object to write text to the cairo Context specified as a parameter.
478472
@@ -495,12 +489,12 @@ def _write_text_pangocairo(self, ctx, text, box_width=None, size=10, fill_color=
495489
pctx.update_layout(pango_layout)
496490

497491
pango_layout.set_text(t)
498-
pctx.set_source_rgb(*fill_color)
492+
pctx.set_source_rgb(*stroke_color)
499493
pctx.show_layout(pango_layout)
500494

501495
return pango_layout.get_pixel_extents()[0]
502496

503-
def _write_text_cairo(self, ctx, text, size=10):
497+
def _write_text_cairo(self, ctx, text, size=10, stroke_color=(0.0, 0.0, 0.0)):
504498
"""
505499
Writes text to the cairo Context specified as a parameter.
506500
@@ -513,6 +507,7 @@ def _write_text_cairo(self, ctx, text, size=10):
513507
cairo.FONT_SLANT_NORMAL,
514508
cairo.FONT_WEIGHT_NORMAL)
515509
ctx.set_font_size(size)
510+
ctx.set_source_rgb(*stroke_color)
516511
ctx.show_text(text)
517512

518513
ctx.rel_move_to(0, size)
@@ -624,16 +619,18 @@ def _get_meta_info_corner(self, render_size, m):
624619
sensible place to render metadata such as a legend or scale.
625620
"""
626621
(x, y) = self._get_render_corner(render_size, m)
622+
623+
render_box_padding_in_meters = 0.005
627624
if self._is_map_size_constrained(m):
628-
y += render_size[1] + 0.005
625+
y += render_size[1] + render_box_padding_in_meters
629626
x = self._margin
630627
else:
631-
x += render_size[0] + 0.005
628+
x += render_size[0] + render_box_padding_in_meters
632629
y = self._margin
633630

634631
return (x, y)
635632

636-
def render_on_map_lat_lon_grid(self, m, dec_degrees=True):
633+
def render_on_map_lat_lon_grid(self, m, dec_degrees=True, grid_layer_name="Latitude Longitude Grid Overlay"):
637634
# FIXME: buggy. does not get the top and right lines. see _render_lat_lon_axis also
638635

639636
"""Renders a lat lon grid on the map."""
@@ -644,7 +641,7 @@ def render_on_map_lat_lon_grid(self, m, dec_degrees=True):
644641
p2 = Projection(m.srs)
645642
latlon_bounds = p2.inverse(m.envelope())
646643

647-
# TODO: comment
644+
# ensure that the projected map envelope is within the lat lon bounds and shift if necessary
648645
latlon_bounds = self._adjust_latlon_bounds(m, p2, latlon_bounds)
649646

650647
latlon_mapwidth = latlon_bounds.width()
@@ -680,10 +677,14 @@ def render_on_map_lat_lon_grid(self, m, dec_degrees=True):
680677
dec_degrees,
681678
False)
682679

680+
if self._use_ocg_layers:
681+
self._surface.show_page()
682+
self._layer_names.append(grid_layer_name)
683+
683684
def _adjust_latlon_bounds(self, m, proj, latlon_bounds):
684685
"""
685686
Ensures that the projected map envelope is within the lat lon bounds.
686-
If it's not it shifts the lat lon bounds.
687+
If it's not, it shifts the lat lon bounds in the right direction by 360 degrees.
687688
688689
Returns:
689690
The adjusted lat lon bounds box
@@ -746,7 +747,7 @@ def _render_lat_lon_axes(self, m, p2, latlon_bounds, latlon_buffer,
746747
temp = m.view_transform().forward(p2.forward(Coord(yvalue, xvalue)))
747748
end = Coord(m2pt(self.map_box.height()) - temp.y, temp.x)
748749

749-
self._draw_line(ctx, start.x, start.y, end.x, end.y)
750+
self._draw_line(ctx, start.x, start.y, end.x, end.y, line_width=0.5)
750751

751752
if cmp(start.y, 0) != cmp(end.y, 0):
752753
start_cross = end.x
@@ -813,7 +814,7 @@ def render_legend(self, m, ctx=None, columns=2, width=None, height=None, attribu
813814

814815
# TODO: refactor that to reduce the number of arguments?
815816
(render_box.width, render_box.height) = self._render_legend_items(m, ctx, render_box, column_width, height,
816-
columns, attribution, legend_item_box_size)
817+
columns=columns, attribution=attribution, legend_item_box_size=legend_item_box_size)
817818

818819
return (render_box.width, render_box.height)
819820

@@ -947,9 +948,8 @@ def _get_layer_style_valid_rules(self, m, layer_style):
947948
for sym in r.symbols:
948949
try:
949950
sym.avoid_edges = False
950-
except:
951-
print(
952-
"**** Cant set avoid edges for rule", r.name)
951+
except AttributeError:
952+
logging.warning("Could not set avoid_edges for rule {}".format(r.name))
953953
if r.min_scale <= m.scale_denominator() and m.scale_denominator() < r.max_scale:
954954
legend_rule = r
955955
legend_rule.min_scale = 0
@@ -958,7 +958,7 @@ def _get_layer_style_valid_rules(self, m, layer_style):
958958

959959
return legend_style
960960

961-
def _render_legend_item_map(self, lemap, legend_map_size, ctx, x, y, current_column, column_width):
961+
def _render_legend_item_map(self, lemap, legend_map_size, ctx, x, y, current_column, column_width, stroke_color=(0.5, 0.5, 0.5), line_width=1):
962962
"""Renders the legend item map."""
963963
ctx.save()
964964
ctx.translate(x + m2pt(current_column * column_width), y)
@@ -969,8 +969,8 @@ def _render_legend_item_map(self, lemap, legend_map_size, ctx, x, y, current_col
969969
ctx.restore()
970970

971971
ctx.rectangle(0, 0, *legend_map_size)
972-
ctx.set_source_rgb(0.5, 0.5, 0.5)
973-
ctx.set_line_width(1)
972+
ctx.set_source_rgb(*stroke_color)
973+
ctx.set_line_width(line_width)
974974
ctx.stroke()
975975
ctx.restore()
976976

@@ -983,23 +983,26 @@ def _render_legend_item_text(self, ctx, legend_map_size, legend_item_box_size, c
983983
the legend text height depending on which one takes more vertical
984984
space.
985985
"""
986-
legend_entry_size = legend_map_size[1]
986+
gray_rgb = (0.5, 0.5, 0.5)
987+
legend_box_padding_in_meters = 0.005
988+
legend_box_width = m2pt(column_width - legend_item_box_size[0] - legend_box_padding_in_meters)
987989

990+
legend_entry_size = legend_map_size[1]
988991
legend_text_size = 0
992+
989993
rule_text = layer_title
990994
if rule_text:
991-
e = self.write_text(ctx, rule_text, m2pt(column_width - legend_item_box_size[0] - 0.005), 6)
995+
e = self.write_text(ctx, rule_text, box_width=legend_box_width, size=6)
992996
legend_text_size += e[3]
993997
ctx.rel_move_to(0, e[3])
994998
if attribution:
995999
if layer_title in attribution:
9961000
e = self.write_text(
9971001
ctx,
9981002
attribution[layer_title],
999-
m2pt(column_width - legend_item_box_size[0] - 0.005),
1000-
6,
1001-
fill_color=(0.5, 0.5, 0.5)
1002-
)
1003+
box_width=legend_box_width,
1004+
size=6,
1005+
stroke_color=gray_rgb)
10031006
legend_text_size += e[3]
10041007

10051008
if legend_text_size > legend_entry_size:
@@ -1035,8 +1038,7 @@ def finish(self):
10351038
["Legend and Information"],
10361039
reverse_all_but_last=True)
10371040

1038-
def convert_pdf_pages_to_layers(
1039-
self, filename, output_name=None, layer_names=None, reverse_all_but_last=True):
1041+
def convert_pdf_pages_to_layers(self, filename, layer_names=None, reverse_all_but_last=True):
10401042
"""
10411043
Takes a multi pages PDF as input and converts each page to a layer in a single page PDF.
10421044
@@ -1051,48 +1053,39 @@ def convert_pdf_pages_to_layers(
10511053
will then be copied back over the source file.
10521054
"""
10531055
if not HAS_PYPDF2:
1054-
raise Exception("PyPDF2 not available; PyPDF2 required to convert pdf pages to layers")
1055-
1056-
infile = file(filename, 'rb')
1057-
if output_name:
1058-
outfile = file(output_name, 'wb')
1059-
else:
1060-
(outfd, tmp_file_abs_path) = tempfile.mkstemp(dir=os.path.dirname(filename))
1061-
outfile = os.fdopen(outfd, 'wb')
1056+
raise RuntimeError("PyPDF2 not available; PyPDF2 required to convert pdf pages to layers")
10621057

1063-
file_reader = PdfFileReader(infile)
1064-
file_writer = PdfFileWriter()
1065-
1066-
template_page_size = file_reader.pages[0].mediaBox
1067-
output_pdf = file_writer.addBlankPage(
1068-
width=template_page_size.getWidth(),
1069-
height=template_page_size.getHeight())
1058+
with open(filename, "rb+") as f:
1059+
file_reader = PdfFileReader(f)
1060+
file_writer = PdfFileWriter()
10701061

1071-
content_key = NameObject('/Contents')
1072-
output_pdf[content_key] = ArrayObject()
1062+
template_page_size = file_reader.pages[0].mediaBox
1063+
output_pdf = file_writer.addBlankPage(
1064+
width=template_page_size.getWidth(),
1065+
height=template_page_size.getHeight())
10731066

1074-
resource_key = NameObject('/Resources')
1075-
output_pdf[resource_key] = DictionaryObject()
1067+
content_key = NameObject('/Contents')
1068+
output_pdf[content_key] = ArrayObject()
10761069

1077-
(properties, ocgs) = self._make_ocg_layers(file_reader, file_writer, output_pdf, layer_names)
1070+
resource_key = NameObject('/Resources')
1071+
output_pdf[resource_key] = DictionaryObject()
10781072

1079-
properties_key = NameObject('/Properties')
1080-
output_pdf[resource_key][properties_key] = file_writer._addObject(properties)
1073+
(properties, ocgs) = self._make_ocg_layers(file_reader, file_writer, output_pdf, layer_names)
10811074

1082-
ocproperties = DictionaryObject()
1083-
ocproperties[NameObject('/OCGs')] = ocgs
1075+
properties_key = NameObject('/Properties')
1076+
output_pdf[resource_key][properties_key] = file_writer._addObject(properties)
10841077

1085-
default_view = self._get_pdf_default_view(ocgs, reverse_all_but_last)
1086-
ocproperties[NameObject('/D')] = file_writer._addObject(default_view)
1078+
ocproperties = DictionaryObject()
1079+
ocproperties[NameObject('/OCGs')] = ocgs
10871080

1088-
file_writer._root_object[NameObject('/OCProperties')] = file_writer._addObject(ocproperties)
1081+
default_view = self._get_pdf_default_view(ocgs, reverse_all_but_last)
1082+
ocproperties[NameObject('/D')] = file_writer._addObject(default_view)
10891083

1090-
file_writer.write(outfile)
1091-
outfile.close()
1092-
infile.close()
1084+
file_writer._root_object[NameObject('/OCProperties')] = file_writer._addObject(ocproperties)
10931085

1094-
if not output_name:
1095-
os.rename(tmp_file_abs_path, filename)
1086+
f.seek(0)
1087+
file_writer.write(f)
1088+
f.truncate()
10961089

10971090
def _make_ocg_layers(self, file_reader, file_writer, output_pdf, layer_names=None):
10981091
"""
@@ -1169,12 +1162,14 @@ def add_geospatial_pdf_header(self, m, filename, epsg=None, wkt=None):
11691162
The epsg code or the wkt text of the projection must be provided.
11701163
Must be called *after* the page has had .finish() called.
11711164
"""
1172-
if HAS_PYPDF2 and (epsg or wkt):
1173-
infile = file(filename, 'rb')
1174-
(outfd, tmp_file_abs_path) = tempfile.mkstemp(dir=os.path.dirname(filename))
1175-
outfile = os.fdopen(outfd, 'wb')
1165+
if not HAS_PYPDF2:
1166+
raise RuntimeError("PyPDF2 not available; PyPDF2 required to add geospatial header to PDF")
1167+
1168+
if not any((epsg,wkt)):
1169+
raise RuntimeError("EPSG or WKT required to add geospatial header to PDF")
11761170

1177-
file_reader = PdfFileReader(infile)
1171+
with open(filename, "rb+") as f:
1172+
file_reader = PdfFileReader(f)
11781173
file_writer = PdfFileWriter()
11791174

11801175
# preserve OCProperties at document root if we have one
@@ -1196,10 +1191,9 @@ def add_geospatial_pdf_header(self, m, filename, epsg=None, wkt=None):
11961191

11971192
file_writer.addPage(page)
11981193

1199-
file_writer.write(outfile)
1200-
infile = None
1201-
outfile.close()
1202-
os.rename(tmp_file_abs_path, filename)
1194+
f.seek(0)
1195+
file_writer.write(f)
1196+
f.truncate()
12031197

12041198
def _get_pdf_measure(self, m, gcs):
12051199
"""

0 commit comments

Comments
 (0)