10. 地図のレンダリングと印刷

ヒント

このページのコードスニペットは、以下のインポートが必要です:

 1import os
 2
 3from qgis.core import (
 4    QgsGeometry,
 5    QgsMapSettings,
 6    QgsPrintLayout,
 7    QgsMapSettings,
 8    QgsMapRendererParallelJob,
 9    QgsLayoutItemLabel,
10    QgsLayoutItemLegend,
11    QgsLayoutItemMap,
12    QgsLayoutItemPolygon,
13    QgsLayoutItemScaleBar,
14    QgsLayoutExporter,
15    QgsLayoutItem,
16    QgsLayoutPoint,
17    QgsLayoutSize,
18    QgsUnitTypes,
19    QgsProject,
20    QgsFillSymbol,
21    QgsAbstractValidityCheck,
22    check,
23)
24
25from qgis.PyQt.QtGui import (
26    QPolygonF,
27    QColor,
28)
29
30from qgis.PyQt.QtCore import (
31    QPointF,
32    QRectF,
33    QSize,
34)

入力データを地図として描画せねばならないときには、総じてふたつのアプローチがあります。 QgsMapRendererJob を使って手早く済ませるか、もしくは :class:`QgsLayout ` クラスで地図を構成し、より精密に調整された出力を作成するかです。

10.1. 単純なレンダリング

レンダリングは QgsMapSettings オブジェクトを生成してレンダリング設定を定義し、その設定で QgsMapRendererJob を生成します。後者が結果の画像を作成するために使用されます。

こちらがサンプルです。

 1image_location = os.path.join(QgsProject.instance().homePath(), "render.png")
 2
 3vlayer = iface.activeLayer()
 4settings = QgsMapSettings()
 5settings.setLayers([vlayer])
 6settings.setBackgroundColor(QColor(255, 255, 255))
 7settings.setOutputSize(QSize(800, 600))
 8settings.setExtent(vlayer.extent())
 9
10render = QgsMapRendererParallelJob(settings)
11
12def finished():
13    img = render.renderedImage()
14    # save the image; e.g. img.save("/Users/myuser/render.png","png")
15    img.save(image_location, "png")
16
17render.finished.connect(finished)
18
19# Start the rendering
20render.start()
21
22# The following loop is not normally required, we
23# are using it here because this is a standalone example.
24from qgis.PyQt.QtCore import QEventLoop
25loop = QEventLoop()
26render.finished.connect(loop.quit)
27loop.exec_()

10.2. 異なるCRSを持つレイヤーをレンダリングする

レイヤが複数あり、それぞれのCRSが異なっている場合は、上記の単純な例ではおそらく求める結果は得られません。範囲計算から正しい値を得るためには、明示的に目的のCRSを設定する必要があります。

layers = [iface.activeLayer()]
settings = QgsMapSettings()
settings.setLayers(layers)
settings.setDestinationCrs(layers[0].crs())

10.3. 印刷レイアウトを使用して出力する

印刷レイアウトは、上に示した単純なレンダリングよりも洗練された出力を行いたい場合に非常に便利なツールです。マップビュー、ラベル、凡例、表、その他紙の地図に通常存在する要素で構成される複雑なマップレイアウトを作成することができます。このレイアウトは、PDF、SVG、ラスタ画像にエクスポートしたり、プリンターで直接印刷することができます。

レイアウトは多くのクラスで構成されています。これらはすべてコア・ライブラリに属しています。GUIライブラリにはありませんが、QGISアプリケーションには要素を配置するための便利なGUIがあります。もしあなたが Qt Graphics View framework に馴染みがないのであれば、レイアウトはそれを基礎にしているので、今すぐドキュメントをチェックすることをお勧めします。

レイアウトの中心となるクラスは QgsLayout クラスで、Qtの QGraphicsScene クラスから派生したものです。インスタンスを作成してみましょう:

project = QgsProject.instance()
layout = QgsPrintLayout(project)
layout.initializeDefaults()

これはいくつかのデフォルト設定でレイアウトを初期化します。具体的には、レイアウトに空のA4ページを追加します。initializeDefaults() メソッドを呼び出さずにレイアウトを作成することもできますが、レイアウトにページを追加する作業は自分で行う必要があります。

前のコードでは、GUIには表示されない「一時的な」レイアウトが作成されます。プロジェクト自体を変更することなく、また変更をユーザーに見せることなく、いくつかのアイテムを素早く追加してエクスポートする場合などに便利です。レイアウトをプロジェクトと一緒に保存/復元し、レイアウトマネージャで利用できるようにしたい場合は、次を追加してください:

layout.setName("MyLayout")
project.layoutManager().addLayout(layout)

これで、様々な要素(マップ、ラベル、...)をレイアウトに追加できるようになりました。これらのオブジェクトはすべて QgsLayoutItem クラスを継承したクラスで表現されます。

以下は、レイアウトに追加できる主なレイアウト項目の説明です。

  • map --- ここにカスタムサイズのマップを作成し、現在のマップキャンバスをレンダリングします。

    1map = QgsLayoutItemMap(layout)
    2# Set map item position and size (by default, it is a 0 width/0 height item placed at 0,0)
    3map.attemptMove(QgsLayoutPoint(5,5, QgsUnitTypes.LayoutMillimeters))
    4map.attemptResize(QgsLayoutSize(200,200, QgsUnitTypes.LayoutMillimeters))
    5# Provide an extent to render
    6map.zoomToExtent(iface.mapCanvas().extent())
    7layout.addLayoutItem(map)
    
  • label --- ラベルを表示できます。そのフォント、色、配置及びマージンを変更することが可能です

    label = QgsLayoutItemLabel(layout)
    label.setText("Hello world")
    label.adjustSizeToText()
    layout.addLayoutItem(label)
    
  • legend

    legend = QgsLayoutItemLegend(layout)
    legend.setLinkedMap(map) # map is an instance of QgsLayoutItemMap
    layout.addLayoutItem(legend)
    
  • スケールバー

    1item = QgsLayoutItemScaleBar(layout)
    2item.setStyle('Numeric') # optionally modify the style
    3item.setLinkedMap(map) # map is an instance of QgsLayoutItemMap
    4item.applyDefaultSize()
    5layout.addLayoutItem(item)
    
  • ノードに基づく図形

     1polygon = QPolygonF()
     2polygon.append(QPointF(0.0, 0.0))
     3polygon.append(QPointF(100.0, 0.0))
     4polygon.append(QPointF(200.0, 100.0))
     5polygon.append(QPointF(100.0, 200.0))
     6
     7polygonItem = QgsLayoutItemPolygon(polygon, layout)
     8layout.addLayoutItem(polygonItem)
     9
    10props = {}
    11props["color"] = "green"
    12props["style"] = "solid"
    13props["style_border"] = "solid"
    14props["color_border"] = "black"
    15props["width_border"] = "10.0"
    16props["joinstyle"] = "miter"
    17
    18symbol = QgsFillSymbol.createSimple(props)
    19polygonItem.setSymbol(symbol)
    

レイアウトに項目を追加したら、移動やサイズの変更ができます:

item.attemptMove(QgsLayoutPoint(1.4, 1.8, QgsUnitTypes.LayoutCentimeters))
item.attemptResize(QgsLayoutSize(2.8, 2.2, QgsUnitTypes.LayoutCentimeters))

各項目の周囲にはデフォルトで枠が描かれます。それを取り除くには次のようにします:

# for a composer label
label.setFrameEnabled(False)

レイアウト項目を手作業で作成する以外に、QGISはレイアウトテンプレートをサポートしています。これは基本的に、すべての項目を.qptファイル(XML構文)に保存した組版です。

その組版の準備ができたら(レイアウト項目が作られ、組版に追加されたら)、ラスタ出力やベクタ出力に進むことができます。

10.3.1. レイアウトの有効性をチェックする

レイアウトは相互に接続された項目の集合で構成され、修正中にこれらの接続が壊れてしまったり(削除されたマップに接続された凡例、ソースファイルが見つからない画像アイテムなど)、レイアウト項目にカスタム制約を適用したい場合があります。 QgsAbstractValidityCheck は、これを実現するのに役立ちます。

基本的なチェックはこのようなものです:

@check.register(type=QgsAbstractValidityCheck.TypeLayoutCheck)
def my_layout_check(context, feedback):
  results = ...
  return results

レイアウトマップ項目がウェブメルカトル図法に設定されるたびに警告を投げるチェックです:

 1@check.register(type=QgsAbstractValidityCheck.TypeLayoutCheck)
 2def layout_map_crs_choice_check(context, feedback):
 3  layout = context.layout
 4  results = []
 5  for i in layout.items():
 6    if isinstance(i, QgsLayoutItemMap) and i.crs().authid() == 'EPSG:3857':
 7      res = QgsValidityCheckResult()
 8      res.type = QgsValidityCheckResult.Warning
 9      res.title = 'Map projection is misleading'
10      res.detailedDescription = 'The projection for the map item {} is set to <i>Web Mercator (EPSG:3857)</i> which misrepresents areas and shapes. Consider using an appropriate local projection instead.'.format(i.displayName())
11      results.append(res)
12
13  return results

さらに複雑な例を挙げます。レイアウトマップ項目に、そのマップ項目に表示されている範囲の外でしか有効でないCRSが設定された場合、警告が投げられます:

 1@check.register(type=QgsAbstractValidityCheck.TypeLayoutCheck)
 2def layout_map_crs_area_check(context, feedback):
 3    layout = context.layout
 4    results = []
 5    for i in layout.items():
 6        if isinstance(i, QgsLayoutItemMap):
 7            bounds = i.crs().bounds()
 8            ct = QgsCoordinateTransform(QgsCoordinateReferenceSystem('EPSG:4326'), i.crs(), QgsProject.instance())
 9            bounds_crs = ct.transformBoundingBox(bounds)
10
11            if not bounds_crs.contains(i.extent()):
12                res = QgsValidityCheckResult()
13                res.type = QgsValidityCheckResult.Warning
14                res.title = 'Map projection is incorrect'
15                res.detailedDescription = 'The projection for the map item {} is set to \'{}\', which is not valid for the area displayed within the map.'.format(i.displayName(), i.crs().authid())
16                results.append(res)
17
18    return results

10.3.2. レイアウトをエクスポートする

レイアウトをエクスポートするには、QgsLayoutExporter クラスを使わなければなりません。

1base_path = os.path.join(QgsProject.instance().homePath())
2pdf_path = os.path.join(base_path, "output.pdf")
3
4exporter = QgsLayoutExporter(layout)
5exporter.exportToPdf(pdf_path, QgsLayoutExporter.PdfExportSettings())

PDFファイルの代わりに、個々のSVG又は画像ファイルにエクスポートしたいときは、exportToSvg() 又は exportToImage() を使います。

10.3.3. 地図帳をエクスポートする

地図帳オプションが設定され、有効になっているレイアウトから全てのページをエクスポートしたい場合、エクスポータ(QgsLayoutExporter)の atlas() メソッドを少し調整して使用する必要があります。以下の例では、ページをPNG画像にエクスポートしています:

exporter.exportToImage(layout.atlas(), base_path, 'png', QgsLayoutExporter.ImageExportSettings())

出力は、地図帳で設定された出力ファイル名式を使用して、ベースパスのフォルダに保存されることに注意してください。