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)