岐阜県の公示図書様式-1(A3)広域図と詳細図を作成
岐阜県の公示図書「様式-1(A3)」をQGISとPythonで自動作成する
今回、以前作成した公示図書作成ツールでExcel帳票を出力し、QGISで地点ごとの位置図を一括作成した後、 Pythonで各Excelへ地図画像を自動貼り付けする流れを作成しました。
全体の作業手順
- 公示図書作成ツールで、地点ごとのExcelを作成する
- 作成されたExcelを1つのフォルダへ集める
- QGISで国土地理院ベクトルタイルを背景に設定する
- 対象箇所のポイントデータを準備する
- ポイントデータの「箇所番」フィールドへ箇所番号を入力する
- QGISのPythonコンソールで画像切り出しコードを実行する
- 出力された20万図と2.5万図を、Excelと同じフォルダへ入れる
- Excel画像貼付用のBATを実行する
- 全Excelへ位置図、赤円、基本入力B12の文字を一括反映する
1.公示図書作成ツールでExcelを作成する
最初に、以前作成した公示図書作成ツールを使用し、地点ごとのExcelを作成します。 作成されたExcelを、今回の一括処理用フォルダへ集めます。
GEN0259001_その1.xlsx KEN0259003_その1.xlsx KEN0259004_その1.xlsx G54031K_その1.xlsx
画像との照合には、Excelファイル名の最初のアンダーバーより左側を使用します。
GEN0259001_その1.xlsx → GEN0259001 KEN0259004_その1.xlsx → KEN0259004
2.QGISで国土地理院ベクトルタイルを設定する
QGISの背景地図には、国土地理院のベクトルタイルを使用します。 QGISのブラウザパネルで「ベクタタイル」を右クリックし、「新しい一般接続」を選択して登録します。
| 項目 | 設定 |
|---|---|
| 名前 | 国土地理院ベクター |
| URL | https://cyberjapandata.gsi.go.jp/xyz/experimental_bvmap/{z}/{x}/{y}.pbf |
| 最小ズーム | 4 |
| 最大ズーム | 16 |
地図の線、注記、色などを確認し、不要なレイヤは非表示にしておきます。 画像切り出しコードは、QGISの地図画面で表示中の背景レイヤをそのまま使用します。
EPSG:6675(JGD2011 / 平面直角座標系第VII系)
に設定しました。
3.対象箇所のポイントデータを準備する
地図の中心とするポイントデータを作成します。
今回のコードでは、ポイントレイヤ名を 表紙作成、
ファイル名に使用するフィールド名を 箇所番 としています。
| 項目 | 設定例 |
|---|---|
| レイヤ名 | 表紙作成 |
| ジオメトリ | ポイント |
| 箇所番号フィールド | 箇所番 |
各ポイントの 箇所番 フィールドへ、Excelと対応する箇所番号を入力します。
GEN0259001 KEN0259003 KEN0259004 G54031K
ポイントの位置が、そのまま左右の地図画像の中心になります。 渓流出口、対象区域の中心など、業務で決めた基準位置へポイントを置きます。
4.QGISで20万図と2.5万図を一括出力する
QGISのメニューから「プラグイン」→「Pythonコンソール」を開きます。 コンソール上部の「エディタを表示」を押し、下記コードを貼り付けて実行します。
コードを実行すると出力先フォルダを選択する画面が表示されます。 各ポイントについて、次の2枚が出力されます。
GEN0259001_200k.png GEN0259001_25k.png
| 画像 | 縮尺 | 用途 |
|---|---|---|
_200k.png |
1:200,000 | 様式-1(A3)の左側 |
_25k.png |
1:25,000 | 様式-1(A3)の右側 |
QGIS:地図画像一括出力コード
import os
import re
from qgis.core import (
QgsProject,
QgsLayout,
QgsLayoutItemMap,
QgsLayoutExporter,
QgsLayoutSize,
QgsLayoutPoint,
QgsUnitTypes,
QgsRectangle,
QgsCoordinateTransform,
QgsWkbTypes,
QgsApplication
)
from qgis.PyQt.QtWidgets import QFileDialog
# =========================================
# 設定
# =========================================
# 出力地点のポイントレイヤ名
POINT_LAYER_NAME = "表紙作成"
# ファイル名に使用するフィールド
NAME_FIELD = "箇所番"
# Excelの地図枠に合わせた出力実寸
PAGE_W_MM = 191.2
PAGE_H_MM = 215.4
# 出力解像度
DPI = 300
# 出力する縮尺
SCALES = {
"200k": 200000,
"25k": 25000
}
# =========================================
# 出力フォルダを選択
# =========================================
OUT_DIR = QFileDialog.getExistingDirectory(
None,
"地図画像の出力先フォルダを選択"
)
if not OUT_DIR:
raise Exception("出力先フォルダが選択されませんでした。")
os.makedirs(OUT_DIR, exist_ok=True)
# =========================================
# プロジェクトとポイントレイヤを取得
# =========================================
project = QgsProject.instance()
point_layers = project.mapLayersByName(POINT_LAYER_NAME)
if not point_layers:
raise Exception(
f"ポイントレイヤ「{POINT_LAYER_NAME}」が見つかりません。"
)
point_layer = point_layers[0]
if not point_layer.isValid():
raise Exception(
f"ポイントレイヤ「{POINT_LAYER_NAME}」が無効です。"
)
if NAME_FIELD not in point_layer.fields().names():
raise Exception(
f"フィールド「{NAME_FIELD}」が見つかりません。"
)
if point_layer.wkbType() == QgsWkbTypes.NoGeometry:
raise Exception("ポイントレイヤに位置情報がありません。")
# =========================================
# 表示中の背景レイヤを取得
# ポイントレイヤ自体は画像へ出さない
# =========================================
visible_layers = [
layer
for layer in iface.mapCanvas().layers()
if layer.id() != point_layer.id()
]
if not visible_layers:
raise Exception(
"背景として出力する表示中レイヤがありません。"
)
print("画像に出力するレイヤ")
for layer in visible_layers:
print(" ・" + layer.name())
# =========================================
# 座標系の確認と変換
# =========================================
project_crs = project.crs()
layer_crs = point_layer.crs()
# 縮尺計算はメートル単位の座標系を前提とする
if project_crs.isGeographic():
raise Exception(
"プロジェクト座標系が緯度経度です。"
"JGD2011の平面直角座標系など、"
"メートル単位の座標系に変更してください。"
)
coordinate_transform = None
if layer_crs != project_crs:
coordinate_transform = QgsCoordinateTransform(
layer_crs,
project_crs,
project
)
# =========================================
# ファイル名を整える
# =========================================
def safe_filename(value):
if value is None:
return "no_name"
text = str(value).strip()
text = re.sub(
r'[\\/:*?"<>|]+',
"_",
text
)
return text if text else "no_name"
# =========================================
# 中心座標と縮尺から表示範囲を作る
# =========================================
def extent_from_center(
center_x,
center_y,
scale,
width_mm,
height_mm
):
ground_width = (
width_mm / 1000.0
) * scale
ground_height = (
height_mm / 1000.0
) * scale
return QgsRectangle(
center_x - ground_width / 2.0,
center_y - ground_height / 2.0,
center_x + ground_width / 2.0,
center_y + ground_height / 2.0
)
# =========================================
# 余白なしのレイアウトを作る
# =========================================
def create_layout():
layout = QgsLayout(project)
layout.initializeDefaults()
page = layout.pageCollection().page(0)
page.setPageSize(
QgsLayoutSize(
PAGE_W_MM,
PAGE_H_MM,
QgsUnitTypes.LayoutMillimeters
)
)
map_item = QgsLayoutItemMap(layout)
layout.addLayoutItem(map_item)
map_item.attemptMove(
QgsLayoutPoint(
0,
0,
QgsUnitTypes.LayoutMillimeters
)
)
map_item.attemptResize(
QgsLayoutSize(
PAGE_W_MM,
PAGE_H_MM,
QgsUnitTypes.LayoutMillimeters
)
)
map_item.setLayers(visible_layers)
map_item.setFrameEnabled(False)
return layout, map_item
# =========================================
# ポイントごとに画像を出力
# =========================================
success_count = 0
error_count = 0
skip_count = 0
features = list(point_layer.getFeatures())
print("")
print(f"対象ポイント数:{len(features)}")
print("出力開始")
print("")
for feature in features:
geometry = feature.geometry()
if geometry is None or geometry.isEmpty():
print(
f"[スキップ] 位置情報なし "
f"Feature ID={feature.id()}"
)
skip_count += 1
continue
file_name = safe_filename(
feature[NAME_FIELD]
)
# ポイント位置を取得
if geometry.type() == QgsWkbTypes.PointGeometry:
if geometry.isMultipart():
points = geometry.asMultiPoint()
if not points:
print(
f"[スキップ] ポイント取得不可:"
f"{file_name}"
)
skip_count += 1
continue
point = points[0]
else:
point = geometry.asPoint()
else:
centroid = geometry.centroid()
if centroid.isEmpty():
print(
f"[スキップ] 中心点取得不可:"
f"{file_name}"
)
skip_count += 1
continue
point = centroid.asPoint()
if coordinate_transform:
point = coordinate_transform.transform(point)
center_x = point.x()
center_y = point.y()
# 20万図と2.5万図を出力
for suffix, scale in SCALES.items():
layout, map_item = create_layout()
extent = extent_from_center(
center_x,
center_y,
scale,
PAGE_W_MM,
PAGE_H_MM
)
map_item.setExtent(extent)
map_item.setScale(scale)
map_item.refresh()
QgsApplication.processEvents()
output_path = os.path.join(
OUT_DIR,
f"{file_name}_{suffix}.png"
)
export_settings = (
QgsLayoutExporter.ImageExportSettings()
)
export_settings.dpi = DPI
export_settings.cropToContents = False
exporter = QgsLayoutExporter(layout)
result = exporter.exportToImage(
output_path,
export_settings
)
if result == QgsLayoutExporter.Success:
print(
f"[完了] {file_name}_{suffix}.png"
)
success_count += 1
else:
print(
f"[失敗] {file_name}_{suffix}.png "
f"結果コード={result}"
)
error_count += 1
# =========================================
# 結果
# =========================================
print("")
print("==================")
print("地図画像の出力が完了しました")
print("==================")
print(f"成功:{success_count}枚")
print(f"失敗:{error_count}枚")
print(f"スキップ:{skip_count}箇所")
print(f"出力先:{OUT_DIR}")
5.Excelと地図画像を1つのフォルダへ集める
QGISで出力したPNGを、公示図書作成ツールで作成したExcelと同じフォルダへ入れます。 Excelはすべて閉じておきます。
作業フォルダ ├─ GEN0259001_その1.xlsx ├─ GEN0259001_200k.png ├─ GEN0259001_25k.png ├─ KEN0259004_その1.xlsx ├─ KEN02590040101_200k.png ├─ KEN02590040101_25k.png ├─ Excel地図画像一括貼付_右画像トリミング版.py └─ Excel地図画像一括貼付_右画像トリミング版_実行.bat
画像名に枝番号が付く場合
Excel名と画像名が完全一致しなくても、画像名がExcelの箇所番号で始まっていれば対応します。
Excel KEN0259004_その1.xlsx 画像 KEN02590040101_200k.png KEN02590040101_25k.png
この場合、Excel側の照合キーは KEN0259004 です。
画像名の先頭も KEN0259004 で始まるため、同じ箇所として貼り付けます。
6.Excel画像貼付用コードを実行する
PythonファイルとBATファイルを同じフォルダへ置き、
Excel地図画像一括貼付_右画像トリミング版_実行.bat
をダブルクリックします。
実行後は次の順に操作します。
- ExcelとPNGが入ったフォルダを選択する
- 全Excelの「基本入力」シートB12へ入れる共通文字を入力する
- 実行ボタンを押す
Excelは閉じた状態で問題ありません。 Pythonが各Excelをバックグラウンドで開き、編集、保存、終了まで行います。
自動で行われる処理
| 処理 | 内容 |
|---|---|
| 左側の画像 | 様式-1(A3)のB3:C34へ20万図を貼付 |
| 右側の画像 | 様式-1(A3)のG3:I34へ2.5万図を貼付し、左側を微調整してトリミング |
| 既存図形 | 貼付範囲に重なる古い画像や赤円を先に削除 |
| 赤円 | 左右の地図枠中央へ、直径3cm、赤色、線幅2.25ptの円を追加 |
| 基本入力B12 | 実行時に入力した文字を、全Excelへ反映 |
| バックアップ | 処理前のExcelを日時付きバックアップフォルダへコピー |
Excel:画像一括貼付Pythonコード
# -*- coding: utf-8 -*-
"""
Excel地図画像一括貼付 右画像トリミング版
処理内容
・フォルダを選択
・全Excelの「基本入力」B12へ共通文字を入力
・Excel名の最初の「_」より左側を照合キーにする
・完全一致、または画像名が照合キーで始まるPNGを使用
・様式-1(A3)!B3:C34へ20万図
・様式-1(A3)!G3:I34へ2.5万図
・貼付範囲に重なる既存図形をすべて削除してから貼付
・地図中央へ直径3cm、赤線2.25ptの円を追加
・処理前のExcelをバックアップ
"""
import re
import shutil
import traceback
from datetime import datetime
from pathlib import Path
import tkinter as tk
from tkinter import filedialog, messagebox
# ============================================================
# 設定
# ============================================================
FORM_SHEET = "様式-1(A3)"
INPUT_SHEET = "基本入力"
INPUT_CELL = "B12"
LEFT_RANGE = "B3:C34"
RIGHT_RANGE = "G3:I34"
LEFT_KIND = "200k"
RIGHT_KIND = "25k"
# 完成形の画像サイズ
LEFT_WIDTH_CM = 19.09
LEFT_HEIGHT_CM = 21.51
RIGHT_WIDTH_CM = 19.24
RIGHT_HEIGHT_CM = 21.51
# 右側画像だけに適用する手動調整値
# Excelの記録マクロをそのまま反映
RIGHT_INCREMENT_LEFT_PT = 6.0678740157
RIGHT_SCALE_WIDTH = 0.9888740182
RIGHT_CROP_PICTURE_WIDTH = 545.0
RIGHT_CROP_PICTURE_HEIGHT = 609.0
RIGHT_CROP_OFFSET_X = -3.0
RIGHT_CROP_OFFSET_Y = 0.0
# 赤円
CIRCLE_CM = 3.0
CIRCLE_LINE_PT = 2.25
# Office定数
MSO_FALSE = 0
MSO_TRUE = -1
MSO_SHAPE_OVAL = 9
MSO_BRING_TO_FRONT = 0
MSO_SCALE_FROM_TOP_LEFT = 0
XL_MOVE_AND_SIZE = 1
# ============================================================
# GUI
# ============================================================
def choose_folder():
root = tk.Tk()
root.withdraw()
root.attributes("-topmost", True)
folder = filedialog.askdirectory(
title="ExcelとPNGが入っているフォルダを選択"
)
root.destroy()
return folder
def ask_common_text(default_text=""):
result = {"value": None}
win = tk.Tk()
win.title("基本入力 B12")
win.geometry("760x310")
win.minsize(620, 260)
win.attributes("-topmost", True)
tk.Label(
win,
text="全Excelの「基本入力」B12へ入れる文字を入力してください。",
anchor="w",
justify="left"
).pack(fill="x", padx=12, pady=(12, 6))
text_box = tk.Text(win, wrap="word", height=10)
text_box.pack(fill="both", expand=True, padx=12, pady=4)
text_box.insert("1.0", default_text or "")
text_box.focus_set()
text_box.tag_add("sel", "1.0", "end-1c")
button_frame = tk.Frame(win)
button_frame.pack(fill="x", padx=12, pady=12)
def on_ok():
value = text_box.get("1.0", "end-1c")
if value == "":
answer = messagebox.askyesno(
"確認",
"B12を空欄にして処理しますか?",
parent=win
)
if not answer:
return
result["value"] = value
win.destroy()
def on_cancel():
result["value"] = None
win.destroy()
tk.Button(
button_frame,
text="実行",
width=12,
command=on_ok
).pack(side="right", padx=(6, 0))
tk.Button(
button_frame,
text="キャンセル",
width=12,
command=on_cancel
).pack(side="right")
win.protocol("WM_DELETE_WINDOW", on_cancel)
win.bind("<Control-Return>", lambda event: on_ok())
win.mainloop()
return result["value"]
def show_message(title, text, is_error=False):
root = tk.Tk()
root.withdraw()
root.attributes("-topmost", True)
if is_error:
messagebox.showerror(title, text, parent=root)
else:
messagebox.showinfo(title, text, parent=root)
root.destroy()
# ============================================================
# ファイル照合
# ============================================================
def get_excel_key(excel_path):
"""
KEN0259004_その1.xlsx → KEN0259004
"""
return excel_path.stem.split("_", 1)[0].strip()
def parse_png_name(png_path):
"""
KEN02590040101_200k.png
→ ("KEN02590040101", "200k")
"""
match = re.match(
r"^(.*)_(200k|25k)$",
png_path.stem,
flags=re.IGNORECASE
)
if not match:
return None
return match.group(1), match.group(2).lower()
def find_matching_png(excel_key, image_kind, png_files):
"""
優先順位
1. 完全一致
2. PNGのベース名がExcelキーで始まる
"""
key_lower = excel_key.lower()
kind_lower = image_kind.lower()
exact = []
prefix = []
for png_path in png_files:
parsed = parse_png_name(png_path)
if not parsed:
continue
base_name, found_kind = parsed
if found_kind.lower() != kind_lower:
continue
base_lower = base_name.lower()
if base_lower == key_lower:
exact.append(png_path)
elif base_lower.startswith(key_lower):
extra_length = len(base_lower) - len(key_lower)
prefix.append(
(
extra_length,
png_path.name.lower(),
png_path
)
)
if exact:
return sorted(
exact,
key=lambda path: path.name.lower()
)[0]
if prefix:
prefix.sort(
key=lambda item: (item[0], item[1])
)
return prefix[0][2]
return None
# ============================================================
# Excel補助
# ============================================================
def get_sheet(workbook, requested_name):
candidates = {
requested_name,
requested_name.replace("-", "-"),
requested_name.replace("-", "-"),
}
for worksheet in workbook.Worksheets:
if str(worksheet.Name) in candidates:
return worksheet
raise RuntimeError(
f"シート「{requested_name}」が見つかりません。"
)
def cm_to_points(cm):
return float(cm) * 28.3464566929
def rgb(r, g, b):
return int(r) + int(g) * 256 + int(b) * 65536
def rectangles_intersect(
left1,
top1,
width1,
height1,
left2,
top2,
width2,
height2
):
right1 = left1 + width1
bottom1 = top1 + height1
right2 = left2 + width2
bottom2 = top2 + height2
return not (
right1 <= left2
or right2 <= left1
or bottom1 <= top2
or bottom2 <= top1
)
def shape_intersects_range(shape, target_range):
"""
図形の外接矩形が対象範囲と少しでも重なるか判定する。
はみ出した古い画像も削除できる。
"""
return rectangles_intersect(
float(shape.Left),
float(shape.Top),
float(shape.Width),
float(shape.Height),
float(target_range.Left),
float(target_range.Top),
float(target_range.Width),
float(target_range.Height)
)
def delete_existing_shapes(worksheet, target_ranges):
"""
対象地図範囲へ少しでも重なる図形をすべて削除する。
古い画像、古い赤円、グループ化された図形も対象。
"""
deleted_names = []
for index in range(worksheet.Shapes.Count, 0, -1):
shape = worksheet.Shapes.Item(index)
try:
should_delete = any(
shape_intersects_range(shape, target_range)
for target_range in target_ranges
)
if should_delete:
try:
deleted_names.append(str(shape.Name))
except Exception:
deleted_names.append(f"Shape{index}")
shape.Delete()
except Exception:
continue
return deleted_names
def calculate_centered_position(
target_range,
width_cm,
height_cm
):
"""
指定サイズをセル範囲の中央へ配置する位置を返す。
"""
width_pt = cm_to_points(width_cm)
height_pt = cm_to_points(height_cm)
range_left = float(target_range.Left)
range_top = float(target_range.Top)
range_width = float(target_range.Width)
range_height = float(target_range.Height)
left = range_left + (range_width - width_pt) / 2.0
top = range_top + (range_height - height_pt) / 2.0
return left, top, width_pt, height_pt
def add_picture(
worksheet,
image_path,
target_range,
width_cm,
height_cm,
shape_name
):
"""
完成形の実寸で、対象範囲の中央へ画像を貼る。
"""
left, top, width_pt, height_pt = (
calculate_centered_position(
target_range,
width_cm,
height_cm
)
)
picture = worksheet.Shapes.AddPicture(
str(image_path),
False,
True,
left,
top,
width_pt,
height_pt
)
picture.LockAspectRatio = MSO_FALSE
picture.Left = left
picture.Top = top
picture.Width = width_pt
picture.Height = height_pt
try:
picture.Placement = XL_MOVE_AND_SIZE
except Exception:
pass
try:
picture.Name = shape_name
except Exception:
pass
return picture
def adjust_right_picture(picture):
"""
右側の2.5万図だけ、手動操作を記録したExcelマクロと
同じ値で左側をトリミングする。
VBA相当:
IncrementLeft 6.0678740157
ScaleWidth 0.9888740182, msoFalse, msoScaleFromTopLeft
Crop.PictureWidth = 545
Crop.PictureHeight = 609
Crop.PictureOffsetX = -3
Crop.PictureOffsetY = 0
"""
try:
picture.LockAspectRatio = MSO_FALSE
picture.IncrementLeft(
RIGHT_INCREMENT_LEFT_PT
)
picture.ScaleWidth(
RIGHT_SCALE_WIDTH,
MSO_FALSE,
MSO_SCALE_FROM_TOP_LEFT
)
crop = picture.PictureFormat.Crop
crop.PictureWidth = RIGHT_CROP_PICTURE_WIDTH
crop.PictureHeight = RIGHT_CROP_PICTURE_HEIGHT
crop.PictureOffsetX = RIGHT_CROP_OFFSET_X
crop.PictureOffsetY = RIGHT_CROP_OFFSET_Y
except Exception as exc:
raise RuntimeError(
f"右画像のトリミング処理に失敗しました: {exc}"
) from exc
return picture
def add_center_circle(
worksheet,
target_range,
shape_name
):
"""
セル範囲中央へ直径3cmの赤円を追加。
"""
diameter_pt = cm_to_points(CIRCLE_CM)
center_x = (
float(target_range.Left)
+ float(target_range.Width) / 2.0
)
center_y = (
float(target_range.Top)
+ float(target_range.Height) / 2.0
)
circle = worksheet.Shapes.AddShape(
MSO_SHAPE_OVAL,
center_x - diameter_pt / 2.0,
center_y - diameter_pt / 2.0,
diameter_pt,
diameter_pt
)
circle.LockAspectRatio = MSO_TRUE
circle.Fill.Visible = MSO_FALSE
circle.Line.Visible = MSO_TRUE
circle.Line.ForeColor.RGB = rgb(255, 0, 0)
circle.Line.Weight = CIRCLE_LINE_PT
try:
circle.Placement = XL_MOVE_AND_SIZE
except Exception:
pass
try:
circle.Name = shape_name
except Exception:
pass
try:
circle.ZOrder(MSO_BRING_TO_FRONT)
except Exception:
pass
return circle
def read_default_b12(excel, excel_files):
if not excel_files:
return ""
workbook = None
try:
workbook = excel.Workbooks.Open(
str(excel_files[0]),
UpdateLinks=0,
ReadOnly=True,
IgnoreReadOnlyRecommended=True
)
value = get_sheet(
workbook,
INPUT_SHEET
).Range(INPUT_CELL).Value
return "" if value is None else str(value)
except Exception:
return ""
finally:
if workbook is not None:
workbook.Close(SaveChanges=False)
# ============================================================
# メイン
# ============================================================
def main():
folder_text = choose_folder()
if not folder_text:
return
folder = Path(folder_text)
excel_files = sorted(
[
path
for extension in ("*.xlsx", "*.xlsm")
for path in folder.glob(extension)
if not path.name.startswith("~$")
],
key=lambda path: path.name.lower()
)
png_files = sorted(
[
path
for path in folder.glob("*.png")
if path.is_file()
],
key=lambda path: path.name.lower()
)
if not excel_files:
show_message(
"対象なし",
"選択したフォルダにExcelファイルがありません。",
True
)
return
if not png_files:
show_message(
"対象なし",
"選択したフォルダにPNG画像がありません。",
True
)
return
try:
import pythoncom
import win32com.client
except ImportError:
show_message(
"pywin32が必要です",
"コマンドプロンプトで次を実行してください。\n\n"
"python -m pip install pywin32",
True
)
return
pythoncom.CoInitialize()
excel = None
workbook = None
try:
excel = win32com.client.DispatchEx(
"Excel.Application"
)
excel.Visible = False
excel.DisplayAlerts = False
excel.ScreenUpdating = False
excel.EnableEvents = False
excel.AskToUpdateLinks = False
default_text = read_default_b12(
excel,
excel_files
)
common_text = ask_common_text(
default_text
)
if common_text is None:
return
timestamp = datetime.now().strftime(
"%Y%m%d_%H%M%S"
)
backup_folder = (
folder
/ f"_Excelバックアップ_{timestamp}"
)
backup_folder.mkdir()
logs = []
completed_count = 0
partial_count = 0
error_count = 0
missing_count = 0
for number, excel_path in enumerate(
excel_files,
start=1
):
key = get_excel_key(excel_path)
left_image = find_matching_png(
key,
LEFT_KIND,
png_files
)
right_image = find_matching_png(
key,
RIGHT_KIND,
png_files
)
messages = []
print(
f"[{number}/{len(excel_files)}] "
f"{excel_path.name}",
flush=True
)
try:
shutil.copy2(
excel_path,
backup_folder / excel_path.name
)
workbook = excel.Workbooks.Open(
str(excel_path),
UpdateLinks=0,
ReadOnly=False,
IgnoreReadOnlyRecommended=True
)
input_sheet = get_sheet(
workbook,
INPUT_SHEET
)
form_sheet = get_sheet(
workbook,
FORM_SHEET
)
input_sheet.Range(
INPUT_CELL
).Value = common_text
left_range = form_sheet.Range(
LEFT_RANGE
)
right_range = form_sheet.Range(
RIGHT_RANGE
)
deleted = delete_existing_shapes(
form_sheet,
[left_range, right_range]
)
if deleted:
messages.append(
f"既存図形削除:{len(deleted)}個"
)
left_ok = False
right_ok = False
if left_image is not None:
add_picture(
form_sheet,
left_image,
left_range,
LEFT_WIDTH_CM,
LEFT_HEIGHT_CM,
"AUTO_MAP_LEFT"
)
add_center_circle(
form_sheet,
left_range,
"AUTO_CIRCLE_LEFT"
)
left_ok = True
messages.append(
f"左:{left_image.name}"
)
else:
missing_count += 1
messages.append(
f"左画像なし:"
f"{key}で始まる_{LEFT_KIND}.png"
)
if right_image is not None:
right_picture = add_picture(
form_sheet,
right_image,
right_range,
RIGHT_WIDTH_CM,
RIGHT_HEIGHT_CM,
"AUTO_MAP_RIGHT"
)
# 右側だけ、手動記録マクロと同じトリミングを適用
adjust_right_picture(
right_picture
)
add_center_circle(
form_sheet,
right_range,
"AUTO_CIRCLE_RIGHT"
)
right_ok = True
messages.append(
f"右:{right_image.name}"
)
else:
missing_count += 1
messages.append(
f"右画像なし:"
f"{key}で始まる_{RIGHT_KIND}.png"
)
try:
workbook.Application.Calculate()
except Exception:
pass
workbook.Save()
workbook.Close(SaveChanges=False)
workbook = None
if left_ok and right_ok:
completed_count += 1
status = "完了"
else:
partial_count += 1
status = "一部完了"
logs.append(
f"[{status}] {excel_path.name} - "
+ " / ".join(messages)
)
except Exception as exc:
error_count += 1
logs.append(
f"[エラー] {excel_path.name} - "
f"{type(exc).__name__}: {exc}"
)
if workbook is not None:
try:
workbook.Close(
SaveChanges=False
)
except Exception:
pass
workbook = None
log_path = (
folder
/ f"Excel地図画像貼付_結果_{timestamp}.txt"
)
header = [
"Excel地図画像一括貼付 結果",
f"処理日時: {datetime.now():%Y-%m-%d %H:%M:%S}",
f"対象フォルダ: {folder}",
f"対象Excel: {len(excel_files)}件",
f"完了: {completed_count}件",
f"一部完了: {partial_count}件",
f"エラー: {error_count}件",
f"不足画像: {missing_count}枚",
"",
"画像照合:",
" 完全一致、または画像名がExcelの箇所番号で始まる場合",
"",
"配置とサイズ:",
(
f" {LEFT_RANGE}: _{LEFT_KIND}.png "
f"幅{LEFT_WIDTH_CM}cm 高さ{LEFT_HEIGHT_CM}cm"
),
(
f" {RIGHT_RANGE}: _{RIGHT_KIND}.png "
f"幅{RIGHT_WIDTH_CM}cm 高さ{RIGHT_HEIGHT_CM}cm"
),
"",
"既存図形:",
" 地図範囲へ少しでも重なる図形を削除後に貼付",
"",
f"バックアップ: {backup_folder}",
"",
]
log_path.write_text(
"\n".join(header + logs),
encoding="utf-8-sig"
)
show_message(
"完了",
(
"処理が終わりました。\n\n"
f"完了:{completed_count}件\n"
f"一部完了:{partial_count}件\n"
f"エラー:{error_count}件\n"
f"不足画像:{missing_count}枚\n\n"
f"結果ログ:\n{log_path}\n\n"
f"バックアップ:\n{backup_folder}"
)
)
except Exception:
show_message(
"処理エラー",
traceback.format_exc(),
True
)
finally:
if workbook is not None:
try:
workbook.Close(SaveChanges=False)
except Exception:
pass
if excel is not None:
try:
excel.EnableEvents = True
excel.ScreenUpdating = True
excel.DisplayAlerts = True
excel.Quit()
except Exception:
pass
pythoncom.CoUninitialize()
if __name__ == "__main__":
main()
実行用BATコード
@echo off
chcp 65001 >nul
title Excel地図画像一括貼付 右画像トリミング版
cd /d "%~dp0"
python -c "import win32com.client" >nul 2>&1
if errorlevel 1 (
echo.
echo Excel操作用の pywin32 が入っていません。
echo.
echo 次のコマンドでインストールしてください。
echo python -m pip install pywin32
echo.
pause
exit /b 1
)
python "%~dp0Excel地図画像一括貼付_右画像トリミング版.py"
if errorlevel 1 (
echo.
echo エラーが発生しました。
)
echo.
pause
7.初回だけ必要な設定
Excel操作には pywin32 を使用します。
未導入の場合は、コマンドプロンプトで次を実行します。
python -m pip install pywin32
一度インストールすれば、次回からはBATをダブルクリックするだけです。
8.完成後に確認する項目
- 左側に1:200,000、右側に1:25,000の画像が入っているか
- 各画像の中心が対象ポイントと一致しているか
- 画像が黒枠からはみ出していないか
- 右側画像の左端が完成例と同じ位置でトリミングされているか
- 赤円が左右の地図枠中央にあるか
- 基本入力シートB12の文字が正しいか
- 枝番号付き画像が別のExcelへ誤って貼られていないか
- 印刷プレビューでA3の1ページに収まっているか
まとめ
公示図書作成ツールでExcel作成 → Excelを1フォルダへ集約 → QGISで国土地理院ベクトルタイルを設定 → ポイントデータの「箇所番」を入力 → Pythonで20万図・2.5万図を一括出力 → PNGをExcelと同じフォルダへ格納 → BATを実行 → 様式-1(A3)を一括完成
手作業で地図を1枚ずつ切り出し、Excelへ位置と大きさを合わせて貼り付ける必要がなくなりました。 地点数が多い公示図書ほど、作業時間の短縮と画像の取り違え防止に効果があります。
※国土地理院データの利用条件、出典表記、測量法に基づく承認の要否については、
成果品の用途と発注者の指示に従って確認してください。
※元Excelの列幅、行高、様式が異なる場合は、画像寸法や右側のトリミング値を調整する必要があります。
コメント
コメントを投稿