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...