44
55from __future__ import absolute_import , print_function
66
7+ import logging
78import math
8- import os
9- import tempfile
109
1110import mapnik
1211from 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
1716try :
1817 import cairo
19- HAS_PYCAIRO_MODULE = True
2018except ImportError :
21- HAS_PYCAIRO_MODULE = False
19+ raise ImportError ( "Could not import pycairo; PDF rendering only available when pycairo is available" )
2220
2321try :
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