QGis QML to CartoCSS converter

May 6, 2014   

During a project I needed to rasterize the Top10NL dataset. The result was to be used as a base layer. Instead of defining all the styles myself, I used the (excellent!) styles defined by JW. Tilemill cannot directly use QGis/QML styles, so I wrote a program to convert QML styles to the CartoCSS format which Tilemill does support.

The QML format is an XML-format which is fairly easy to read, while CartoCSS is a format based on CSS. There are several diffrent kinds of objects in the Top10NL, all with multiple styles. So instead of converting the styles myself (boring work and error prone), I wrote a small converter QML to CartoCSS converter.

The converter reads the XML and writes CartoCSS. The structure of the QML type is straight forward, so writing the converter was easy. You can find the sources here. A listing of the code is below:

#!/usr/bin/env python

from lxml import etree
import sys

def process_file(f):
    print('XXX: Processing file: {0}'.format(f))
    basename = f[8:-4]
    print('.{0} {{'.format(basename))

    # open file
    tree = etree.parse(f)
    root = tree.getroot()

    # read attr
    renderer = root.findall('.//renderer-v2')[0]
    if renderer.attrib['type'] == 'categorizedSymbol':
        attr = renderer.attrib['attr']
        process_categorizedSymbol(renderer, attr)
    elif renderer.attrib['type'] == 'singleSymbol':
        process_singleSymbol(root)
    else:
        print('XXX: unknown type')

    print('}')

def process_categorizedSymbol(renderer, attr):
    # attr we look at to determine styles
    # attr -> value: category

    for category in renderer.findall('.//categories//category'):
        symbol_name = category.attrib['symbol']
        value = unicode(category.attrib['value']).encode('utf-8')

        query = './/symbols//symbol[@name="{0}"]'.format(symbol_name)
        for symbol in renderer.findall(query):
            print('  [{0}="{1}"] {{'.format(attr, value))
            process_symbol(symbol)
            print('  }')

def process_singleSymbol(renderer):
    symbol = renderer.findall('.//symbol')[0]
    process_symbol(symbol)

def process_symbol(symbol):
    layer = symbol.findall('.//layer')[0]
    if symbol.attrib['type'] == 'fill':
        # color -> polygon-fill
        color = get_prop_color(layer, 'color')
        print('    polygon-fill: {0};'.format(color))

        # color_border -> line-color
        color_border = get_prop_color(layer, 'color_border')
        print('    line-color: {0};'.format(color_border))

        # width_border -> line-width
        width = get_prop(layer, 'width-border')
        if width:
            print('    line-width: {0};'.format(width))

    if symbol.attrib['type'] == 'line':
        # color -> line-color
        color = get_prop_color(layer, 'color')
        print('    line-color: {0};'.format(color))

        # width -> line-width
        width = get_prop(layer, 'width')
        if width:
            print('    line-width: {0};'.format(width))

        # penstyle -> line-dasharray
        penstyle = get_prop_penstyle(layer, 'penstyle')
        if penstyle and penstyle != 'none':
            print('    line-dasharray: {0};'.format(penstyle))

    if symbol.attrib['type'] == 'marker':
        # color -> marker-fill
        color = get_prop_color(layer, 'color')
        print('    marker-fill: {0};'.format(color))

        # color_border -> marker-line-color
        color_border = get_prop_color(layer, 'color_border')
        print('    marker-line-color: {0};'.format(color_border))

        # size -> marker-width
        size = float(get_prop(layer, 'size')) * 5
        print('    marker-width: {0};'.format(size))

def get_prop(layer, prop):
    return find_layer_prop(layer, prop)

def get_prop_color(layer, prop):
    color = find_layer_prop(layer, prop)
    return 'rgba({0})'.format(color)

def get_prop_penstyle(layer, prop):
    penstyle = find_layer_prop(layer, prop)
    if penstyle == 'solid':
        return 'none'
    if penstyle == 'dash':
        return '5, 2'
    if penstyle == 'dot':
        return '1, 1'
    return 'XXX'

def find_layer_prop(layer, prop):
    query = './/prop[@k="{0}"]'.format(prop)
    results = layer.findall(query)
    if not results:
        return None
    return results[0].attrib['v']

def main():
    process_file(sys.argv[1])

if __name__ == '__main__':
    main()

Please note that this is a minimal script which worked in this situation. It does not cover the full QML specification and thus might need work to be able to convert your QML file.

Please also note that there might be multiple versions of the QML specification being used. The QML files converted in this case have version “1.7.3-Wroclaw”.

Usage:

  • Save the above code listing to qml2cartocss.py
  • Make sure you have Python installed on your system
  • Run the program as follows:

$ python qml2cartocss.py my_qml_file.qml