File indexing completed on 2024-04-21 14:43:51
0001 #! /usr/bin/env python 0002 0003 # 0004 # GCompris - export_layers_gcompris.py 0005 # 0006 # SPDX-FileCopyrightText: 2021 Timothée Giet <animtim@gmail.com> 0007 # 0008 # SPDX-License-Identifier: GPL-3.0-or-later 0009 # 0010 # An Inkscape extension to export svg maps with center coordinates in a text file, 0011 # to generate content for puzzle/maps activities 0012 # based on https://github.com/dja001/inkscape-export-layers 0013 0014 import collections 0015 import contextlib 0016 import copy 0017 import os 0018 import shutil 0019 import subprocess 0020 import sys 0021 import tempfile 0022 0023 sys.path.append('/usr/share/inkscape/extensions') 0024 import inkex 0025 0026 Layer = collections.namedtuple('Layer', ['id', 'label', 'tag']) 0027 Export = collections.namedtuple('Export', ['visible_layers', 'file_name']) 0028 0029 FIXED = '[fixed]' 0030 F = '[f]' 0031 EXPORT = '' 0032 E = '[e]' 0033 BACK = '[back]' 0034 0035 SVG = 'svg' 0036 PNG = 'png' 0037 0038 DOCWIDTH = 0 0039 DOCHEIGHT = 0 0040 DOCNAME = '' 0041 0042 coordinates_string = "" 0043 0044 class LayerExport(inkex.Effect): 0045 def __init__(self): 0046 inkex.Effect.__init__(self) 0047 self.arg_parser.add_argument('-o', '--output-source', 0048 action='store', 0049 type=str, 0050 dest='output_source', 0051 default='~/', 0052 help='Path to source file in output directory') 0053 self.arg_parser.add_argument('--output-subdir', 0054 action='store', 0055 type=str, 0056 dest='output_subdir', 0057 default='', 0058 help='name of sub-directory in output path') 0059 self.arg_parser.add_argument('-f', '--file-type', 0060 action='store', 0061 choices=(SVG, PNG), 0062 dest='file_type', 0063 default='svg', 0064 help='Exported file type') 0065 self.arg_parser.add_argument('--fit-contents', 0066 action='store', 0067 type=str, 0068 dest='fit_contents', 0069 default=True, 0070 help='Fit output to content bounds') 0071 self.arg_parser.add_argument('--dpi', 0072 action='store', 0073 type=int, 0074 dest='dpi', 0075 default=None, 0076 help="Export DPI value") 0077 self.arg_parser.add_argument('--enumerate', 0078 action='store', 0079 type=str, 0080 dest='enumerate', 0081 default=None, 0082 help="suffix of files exported") 0083 self.arg_parser.add_argument('--show-layers-below', 0084 action='store', 0085 type=str, 0086 dest='show_layers_below', 0087 default=None, 0088 help="Show exported layers below the current layer") 0089 0090 def effect(self): 0091 0092 #process bool inputs that were read as strings 0093 self.options.fit_contents = True if self.options.fit_contents == 'true' else False 0094 self.options.enumerate = True if self.options.enumerate == 'true' else False 0095 self.options.show_layers_below = True if self.options.show_layers_below == 'true' else False 0096 0097 #get output dir from specified source file 0098 #otherwise set it as $HOME 0099 source = self.options.output_source 0100 if os.path.isfile(source): 0101 output_dir = os.path.dirname(source) 0102 prefix = os.path.splitext(os.path.basename(source))[0]+'_' 0103 elif os.path.isdir(source): 0104 #change the default filled in by inkscape to $HOME 0105 if os.path.basename(source) == 'inkscape-export-layers': 0106 output_dir = os.path.expanduser('~/') 0107 prefix = '' 0108 else: 0109 output_dir = os.path.join(source) 0110 prefix = '' 0111 else: 0112 raise Exception('output_source not a file or a dir...') 0113 0114 #add subdir if one was passed 0115 output_dir = os.path.join(output_dir, self.options.output_subdir) 0116 0117 if not os.path.exists(output_dir): 0118 os.makedirs(output_dir) 0119 0120 layer_list = self.get_layer_list() 0121 export_background = self.get_export_background(layer_list, self.options.show_layers_below) 0122 export_list = self.get_export_list(layer_list, self.options.show_layers_below) 0123 0124 #get document infos 0125 global DOCWIDTH 0126 DOCWIDTH = float(self.svg.get("width")) 0127 global DOCHEIGHT 0128 DOCHEIGHT = float(self.svg.get("height")) 0129 global DOCNAME 0130 DOCNAME = self.svg.get("sodipodi:docname") 0131 0132 with _make_temp_directory() as tmp_dir: 0133 for export in export_background: 0134 svg_file = self.export_to_svg(export, tmp_dir) 0135 isNotBackground = False 0136 0137 if self.options.file_type == PNG: 0138 if not self.convert_svg_to_png(svg_file, output_dir, prefix, isNotBackground): 0139 break 0140 elif self.options.file_type == SVG: 0141 if not self.convert_svg_to_svg(svg_file, output_dir, prefix, isNotBackground): 0142 break 0143 0144 with _make_temp_directory() as tmp_dir: 0145 for export in export_list: 0146 svg_file = self.export_to_svg(export, tmp_dir) 0147 isNotBackground = True 0148 0149 if self.options.file_type == PNG: 0150 if not self.convert_svg_to_png(svg_file, output_dir, prefix, isNotBackground): 0151 break 0152 elif self.options.file_type == SVG: 0153 if not self.convert_svg_to_svg(svg_file, output_dir, prefix, isNotBackground): 0154 break 0155 0156 coords_text_file = os.path.join(output_dir, DOCNAME + '.txt') 0157 with open(coords_text_file, "w") as text_file: 0158 print("{}".format(coordinates_string), file=text_file) 0159 0160 def get_layer_list(self): 0161 """make a list of layers in source svg file 0162 Elements of the list are of the form (id, label (layer name), tag ('[fixed]' or '[back]' or '[export]') 0163 """ 0164 svg_layers = self.document.xpath('//svg:g[@inkscape:groupmode="layer"]', 0165 namespaces=inkex.NSS) 0166 layer_list = [] 0167 0168 for layer in svg_layers: 0169 label_attrib_name = '{%s}label' % layer.nsmap['inkscape'] 0170 if label_attrib_name not in layer.attrib: 0171 continue 0172 0173 layer_id = layer.attrib['id'] 0174 layer_label = layer.attrib[label_attrib_name] 0175 0176 if layer_label.lower().startswith(FIXED): 0177 layer_type = FIXED 0178 layer_label = layer_label[len(FIXED):].lstrip() 0179 elif layer_label.lower().startswith(F): 0180 layer_type = FIXED 0181 layer_label = layer_label[len(F):].lstrip() 0182 elif layer_label.lower().startswith(BACK): 0183 layer_type = BACK 0184 layer_label = layer_label[len(BACK):].lstrip() 0185 elif layer_label.lower().startswith(EXPORT): 0186 layer_type = EXPORT 0187 layer_label = layer_label[len(EXPORT):].lstrip() 0188 else: 0189 continue 0190 0191 layer_list.append(Layer(layer_id, layer_label, layer_type)) 0192 0193 return layer_list 0194 0195 def get_export_list(self, layer_list, show_layers_below): 0196 """selection of layers that should be visible 0197 0198 Each element of this list will be exported in its own file 0199 """ 0200 export_list = [] 0201 0202 for counter, layer in enumerate(layer_list): 0203 #each layer marked as '[export]' is the basis for making a figure that will be exported 0204 0205 if layer.tag == FIXED: 0206 #Fixed layers are not the basis of exported figures 0207 continue 0208 elif layer.tag == EXPORT: 0209 0210 #determine which other layers should appear in this figure 0211 visible_layers = set() 0212 layer_is_below = True 0213 for other_layer in layer_list: 0214 if other_layer.tag == FIXED: 0215 #fixed layers appear in all figures 0216 #irrespective of their position relative to other layers 0217 visible_layers.add(other_layer.id) 0218 else: 0219 if other_layer.id == layer.id: 0220 #the basis layer for this figure is always visible 0221 visible_layers.add(other_layer.id) 0222 #all subsequent layers will be above 0223 layer_is_below = False 0224 0225 elif layer_is_below and show_layers_below: 0226 visible_layers.add(other_layer.id) 0227 0228 layer_name = layer.label 0229 if self.options.enumerate: 0230 layer_name = '{:03d}_{}'.format(counter + 1, layer_name) 0231 0232 export_list.append(Export(visible_layers, layer_name)) 0233 else: 0234 #layers not marked as FIXED of EXPORT are ignored 0235 pass 0236 0237 return export_list 0238 0239 def get_export_background(self, layer_list, show_layers_below): 0240 """selection of files with [back] tag, 0241 to always render at document boundaries size 0242 """ 0243 export_list = [] 0244 0245 for counter, layer in enumerate(layer_list): 0246 #each layer marked as '[export]' is the basis for making a figure that will be exported 0247 0248 if layer.tag == FIXED: 0249 #Fixed layers are not the basis of exported figures 0250 continue 0251 elif layer.tag == BACK: 0252 0253 #determine which other layers should appear in this figure 0254 visible_layers = set() 0255 layer_is_below = True 0256 for other_layer in layer_list: 0257 if other_layer.tag == FIXED: 0258 #fixed layers appear in all figures 0259 #irrespective of their position relative to other layers 0260 visible_layers.add(other_layer.id) 0261 else: 0262 if other_layer.id == layer.id: 0263 #the basis layer for this figure is always visible 0264 visible_layers.add(other_layer.id) 0265 #all subsequent layers will be above 0266 layer_is_below = False 0267 0268 elif layer_is_below and show_layers_below: 0269 visible_layers.add(other_layer.id) 0270 0271 layer_name = layer.label 0272 if self.options.enumerate: 0273 layer_name = '{:03d}_{}'.format(counter + 1, layer_name) 0274 0275 export_list.append(Export(visible_layers, layer_name)) 0276 else: 0277 #layers not marked as FIXED of BACK are ignored 0278 pass 0279 0280 return export_list 0281 0282 def export_to_svg(self, export, output_dir): 0283 """ 0284 Export a current document to an Inkscape SVG file. 0285 :arg Export export: Export description. 0286 :arg str output_dir: Path to an output directory. 0287 :return Output file path. 0288 """ 0289 document = copy.deepcopy(self.document) 0290 0291 svg_layers = document.xpath('//svg:g[@inkscape:groupmode="layer"]', 0292 namespaces=inkex.NSS) 0293 0294 content_bb = [] 0295 content_x1 = 0 0296 content_x2 = 0 0297 content_y1 = 0 0298 content_y2 = 0 0299 0300 for layer in svg_layers: 0301 if layer.attrib['id'] in export.visible_layers: 0302 layer.attrib['style'] = 'display:inline' 0303 self.svg.selection = layer.descendants() 0304 content_bb = self.svg.selection.first().bounding_box() 0305 else: 0306 layer.delete() 0307 0308 #find coords and size of selection 0309 content_x1 = content_bb.left 0310 content_x2 = content_bb.right 0311 content_y1 = content_bb.top 0312 content_y2 = content_bb.bottom 0313 0314 content_x_center = (((content_x2 - content_x1) / 2) + content_x1) / DOCWIDTH 0315 content_y_center = (((content_y2 - content_y1) / 2) + content_y1) / DOCHEIGHT 0316 0317 content_x_center = round(content_x_center, 4) 0318 content_y_center= round(content_y_center, 4) 0319 0320 #example to write coordinates to string... 0321 global coordinates_string 0322 coordinates_string += export.file_name 0323 coordinates_string += ", X: " 0324 coordinates_string += str(content_x_center) 0325 coordinates_string += ", Y: " 0326 coordinates_string += str(content_y_center) 0327 coordinates_string += "\n" 0328 0329 output_file = os.path.join(output_dir, export.file_name + '.svg') 0330 document.write(output_file) 0331 0332 return output_file 0333 0334 def convert_svg_to_png(self, svg_file, output_dir, prefix, isNotBackground): 0335 """ 0336 Convert an SVG file into a PNG file. 0337 :param str svg_file: Path an input SVG file. 0338 :param str output_dir: Path to an output directory. 0339 :return Output file path. 0340 """ 0341 source_file_name = os.path.splitext(os.path.basename(svg_file))[0] 0342 output_file = os.path.join(output_dir, prefix+source_file_name + '.png') 0343 command = [ 0344 'inkscape', 0345 svg_file.encode('utf-8'), 0346 '--batch-process', 0347 '--export-area-drawing' if self.options.fit_contents and isNotBackground else 0348 '--export-area-page', 0349 '--export-dpi', str(self.options.dpi), 0350 '--export-type', 'png', 0351 '--export-filename', output_file.encode('utf-8'), 0352 ] 0353 result = subprocess.run(command, capture_output=True) 0354 if result.returncode != 0: 0355 raise Exception('Failed to convert %s to PNG' % svg_file) 0356 0357 return output_file 0358 0359 def convert_svg_to_svg(self, svg_file, output_dir, prefix, isNotBackground): 0360 """ 0361 Convert an [Inkscape] SVG file into a standard (plain) SVG file. 0362 :param str svg_file: Path an input SVG file. 0363 :param str output_dir: Path to an output directory. 0364 :return Output file path. 0365 """ 0366 source_file_name = os.path.splitext(os.path.basename(svg_file))[0] 0367 output_file = os.path.join(output_dir, prefix+source_file_name + '.svg') 0368 command = [ 0369 'inkscape', 0370 svg_file.encode('utf-8'), 0371 '--batch-process', 0372 '--export-area-drawing' if self.options.fit_contents and isNotBackground else 0373 '--export-area-page', 0374 '--export-dpi', str(self.options.dpi), 0375 '--export-plain-svg', 0376 '--vacuum-defs', 0377 '--export-filename',output_file.encode('utf-8') 0378 ] 0379 result = subprocess.run(command, capture_output=True) 0380 if result.returncode != 0: 0381 raise Exception('Failed to convert %s to SVG' % svg_file) 0382 0383 return output_file 0384 0385 0386 @contextlib.contextmanager 0387 def _make_temp_directory(): 0388 temp_dir = tempfile.mkdtemp(prefix='tmp-inkscape') 0389 try: 0390 yield temp_dir 0391 finally: 0392 shutil.rmtree(temp_dir) 0393 0394 0395 if __name__ == '__main__': 0396 try: 0397 LayerExport().run(output=False) 0398 except Exception as e: 0399 inkex.errormsg(str(e)) 0400 sys.exit(1)