1. This site uses cookies. By continuing to use this site, you are agreeing to our use of cookies. Learn More.
  2. Anuncie Aqui
    Anuncie aqui você Também: fdantas@4each.com.br

[Python] QGIS headless + Python - exporting PDF with XYZ tiles layer

Discussão em 'Python' iniciado por Stack, Setembro 8, 2025.

  1. Stack

    Stack Membro Participativo

    I have a script that generates a PDF file from QGIS headless running on Ubuntu. The script generates a map view from a geojson feature which is centered on the view and a Mapbox XYZ tile layer is put behind it.

    When running the script for the first time, I get a segmentation error and a faulty PDF is generated. The second run of the same script produces a correct PDF.

    What might be happening is, that the PDF is being exported while the XYZ tiles are not fully loaded. The second run uses cached tiles and no error ocurres. But correct me if I'm wrong.

    So my question is, how to tell when the tiles are fully loaded and it is safe to generate the final PDF? (Same happens when generating a PNG output)

    This is my script:

    import os
    os.environ["QT_QPA_PLATFORM"] = "offscreen"
    os.environ["QGIS_DISABLE_CACHE"] = "1"

    import sys

    from qgis.PyQt.QtGui import QColor
    from qgis.PyQt.QtXml import QDomDocument


    from qgis.core import (
    QgsApplication,
    QgsVectorLayer,
    QgsRasterLayer,
    QgsProject,
    QgsCoordinateReferenceSystem,
    QgsPrintLayout,
    QgsLayoutItemMap,
    QgsLayoutExporter,
    QgsReadWriteContext,
    QgsCoordinateTransform,
    QgsNetworkAccessManager
    )

    printTemplatePath = os.path.join("assets", "print_template.qpt")


    MAPBOX_MAP_TOKEN = "***"

    props = {
    "color": [255, 16, 16],
    "opacity": 0.75,
    "strokeColor": [255, 36, 36],
    "strokeWidth": 0.5,
    }

    def loadLayer(geojsonPath):
    layer = QgsVectorLayer(geojsonPath, "geojsonLayer", "ogr")
    if not layer.isValid():
    print(f"Failed to load: {geojsonPath}")
    qgs.exitQgis()
    sys.exit(1)
    QgsProject.instance().addMapLayer(layer)
    return layer


    def setSymbology(layer):
    singleSymbolRenderer = layer.renderer()
    symbol = singleSymbolRenderer.symbol()
    symbol.setColor(QColor.fromRgb(props['color'][0], props['color'][1], props['color'][2]))
    symbol.setOpacity(props['opacity'])
    symbol.symbolLayer(0).setStrokeColor(QColor(props['strokeColor'][0], props['strokeColor'][1], props['strokeColor'][2]))
    symbol.symbolLayer(0).setStrokeWidth(props['strokeWidth'])

    layer.triggerRepaint()


    def addMapboxLayer():
    username = "***"
    style_id = "***"

    try:
    uri = f"type=xyz&url=https://api.mapbox.com/styles/v1/{username}/{style_id}/tiles/256/{{z}}/{{x}}/{{y}}?access_token={MAPBOX_MAP_TOKEN}"

    layer = QgsRasterLayer(uri, "Mapbox Style", "wms")

    QgsProject.instance().addMapLayer(layer)
    root = QgsProject.instance().layerTreeRoot()
    node = root.findLayer(layer.id())
    clone = node.clone()
    root.insertChildNode(-1, clone)
    root.removeChildNode(node)
    except Exception as e:
    print(f"Error with tile layer: {e}")
    exit(4)

    def waitForMapReady():
    # need to determine when all the tiles are loaded
    pass

    def setProjectCrs():
    crs = QgsCoordinateReferenceSystem("EPSG:8857")
    QgsProject.instance().setCrs(crs)


    def getAdjustedLayerExtentForMapFrame(mapItem, polygons, layerCrs, padding):
    frameRect = mapItem.rect()
    targetAspect = frameRect.width() / frameRect.height()

    # combine extents of all polygons
    combinedExtent = None
    for geom in polygons:
    if geom and not geom.isEmpty():
    if combinedExtent is None:
    combinedExtent = geom.boundingBox()
    else:
    combinedExtent.combineExtentWith(geom.boundingBox())

    # transform to project CRS if needed
    projectCrs = QgsProject.instance().crs()
    if layerCrs != projectCrs:
    tr = QgsCoordinateTransform(layerCrs, projectCrs, QgsProject.instance())
    extentProj = tr.transformBoundingBox(combinedExtent)
    else:
    extentProj = combinedExtent

    # apply padding
    extentProj.setXMinimum(extentProj.xMinimum() - extentProj.width()*(padding-1)/2)
    extentProj.setXMaximum(extentProj.xMaximum() + extentProj.width()*(padding-1)/2)
    extentProj.setYMinimum(extentProj.yMinimum() - extentProj.height()*(padding-1)/2)
    extentProj.setYMaximum(extentProj.yMaximum() + extentProj.height()*(padding-1)/2)

    # adjust to match map frame aspect ratio
    extentWidth = extentProj.width()
    extentHeight = extentProj.height()
    layerAspect = extentWidth / extentHeight

    if layerAspect > targetAspect:
    newHeight = extentWidth / targetAspect
    yCenter = (extentProj.yMinimum() + extentProj.yMaximum()) / 2
    extentProj.setYMinimum(yCenter - newHeight/2)
    extentProj.setYMaximum(yCenter + newHeight/2)
    else:
    newWidth = extentHeight * targetAspect
    xCenter = (extentProj.xMinimum() + extentProj.xMaximum()) / 2
    extentProj.setXMinimum(xCenter - newWidth/2)
    extentProj.setXMaximum(xCenter + newWidth/2)

    return extentProj


    def printPdf(geojsonLayer, features, templatePath, outputFileName):
    project = QgsProject.instance()
    layout = QgsPrintLayout(project)
    layout.initializeDefaults()

    with open(templatePath) as f:
    templateContent = f.read()
    doc = QDomDocument()
    doc.setContent(templateContent)
    context = QgsReadWriteContext()
    layout.loadFromTemplate(doc, context)

    mapItems = [item for item in layout.items() if isinstance(item, QgsLayoutItemMap)]
    if not mapItems:
    raise Exception("No map item found in layout!")
    mapItem = mapItems[0]

    polygons = [f.geometry() for f in features if f.hasGeometry()]
    if not polygons:
    raise ValueError("No valid geometries found in features")

    # Adjust map extent
    adjustedExtent = getAdjustedLayerExtentForMapFrame(mapItem, polygons, geojsonLayer.crs(), padding=1.2)
    mapItem.setExtent(adjustedExtent)
    mapItem.refresh()

    waitForMapReady()

    exporter = QgsLayoutExporter(layout)
    pdfSettings = QgsLayoutExporter.PdfExportSettings()
    pdfSettings.appendGeoreference = False
    pdfSettings.exportMetadata = False
    pdfSettings.rasterizeWholeImage = True
    pdfSettings.dpi = 144
    pdfSettings.imageCompression = "JPEG"
    pdfSettings.quality = 70
    if exporter.exportToPdf(outputFileName, pdfSettings) == QgsLayoutExporter.Success:
    print(f"Exported PDF: {outputFileName}")
    return outputFileName
    else:
    print(f"Failed to export PDF: {pdfPath}")

    def main():
    if len(sys.argv) < 3:
    print("Usage: python3 <inputJson> <outputFileName>")
    sys.exit(1)

    inputJson = sys.argv[1]
    outputFileName = sys.argv[2]

    qgs = QgsApplication([], False)
    QgsApplication.setPrefixPath("/usr", True)
    QgsApplication.setMaxThreads(1)
    QgsNetworkAccessManager.instance()
    qgs.initQgis()

    addMapboxLayer()
    geojsonLayer = loadLayer(inputJson)
    setSymbology(geojsonLayer)
    setProjectCrs()
    features = [f for f in geojsonLayer.getFeatures()]
    printPdf(geojsonLayer, features, printTemplatePath, outputFileName)

    qgs.exitQgis()

    if __name__ == "__main__":
    main()

    Continue reading...

Compartilhe esta Página