This is the first time in over three decades of existence that I cannot find an answer and I actually have to ask one for myself, so please accept my apologies for any formatting issues or lack of clarity.

I have made a point layer using this guide https://www.fulcrumapp.com/blog/photos-in-qgis to import my geotagged photos so they have a Field of View cone. I've labelled them with the photo file name.

FOV geotagged photos

What I'd like to do is make a reference appendix that has the resized photos in a print layout that automatically populates the photos in an image with a text box referencing the file name per the below.

print composer of images

Then have it auto populate the text box with the file name of the photo.

I cannot seem to find a guide to make this, but I think it's been done before.

  • 1
    Does it have to be using qgis? atlas function is rather for making a map for every single feature of a layer, here you want to put many features in a single map, i'd do it in R or Python; for what i can see there's no geospatial component in the appendix you want to build – Elio Diaz Jan 5 at 0:02
  • I agree with @ElioDiaz. It looks like so hard without any script. – Kadir Şahbaz Jan 5 at 0:12
  • Probably the wrong place to admit this, but I know zero coding. – MattimusAurelius Jan 5 at 0:22

It looks like so hard without any script. So I tried a solution like below.

Sample data:

enter image description here

margin and interval

enter image description here


Copy/paste the script to QGIS Python Editor. Set layout_name, layer_name, path_field, rc, cc and run.

import os
from qgis.PyQt.QtGui import QFont

layout_name = "LAYOUT_NAME"
layer_name = "Images"
path_field = "img_path"
rc, cc = 2, 2 # rc(row_count), cc(column_count)
interval = 10
margin = 20

project = QgsProject.instance()

# layout settings
layout_manager = project.layoutManager()
layouts = layout_manager.printLayouts()

# remove the same named layout 
for layout in layouts:
    if layout.name() == layout_name:

layout = QgsPrintLayout(project)


layer = project.mapLayersByName(layer_name)[0]
n = layer.featureCount()

### add pages
pcoll = layout.pageCollection()
pc = int(n /(cc*rc)) # required page count
for i in range(pc):
    page = QgsLayoutItemPage(layout)
    page.setPageSize('A4', QgsLayoutItemPage.Landscape)

pw = pcoll.pages()[0].pageSize().width() # page width 
ph = pcoll.pages()[0].pageSize().height() # page height

w = (pw - (cc - 1) * interval - 2* margin) / cc # image width
h = (ph - (rc - 1) * interval - 2* margin) / rc # image height
print(w, h)
for i, f in enumerate(layer.getFeatures()):
    r = (int(i / rc) % cc) # current row number, 0 indexed
    c = i % cc # current column number, 0 indexed
    pn = int(i / (cc * rc)) # current page number, 0 indexed

    # calculate top-left coordinate for the image
    x = margin + c * (interval + w)
    y = margin + r * (interval + h)

    # add image
    img =  QgsLayoutItemPicture(layout)
    img.attemptMove(QgsLayoutPoint(x, y, 0), page=pn) 
    img.attemptResize(QgsLayoutSize(w, h, 0))
    # calculate top-left coordinate for the label
    x = margin + c * (interval + w)
    y = margin + r * (interval + h) + h

    # add label
    label = QgsLayoutItemLabel(layout)
    label.setFont(QFont('Arial', 14))
    label.attemptMove(QgsLayoutPoint(x, y, 0), page=pn) 
    label.attemptResize(QgsLayoutSize(w, interval, 0))


enter image description here

  • This looks like a small plugin, i'll bookmark it – Elio Diaz Jan 8 at 20:19
  • More stable script was added. – Kadir Şahbaz Jan 11 at 13:07

I know you mentioned you do not code, but your task may be cumbersome in click and drag software, because you want paired images-filenames; your goal may be accomplished with very few (8 to be precise) lines of code, such as the following; you just install R from https://cran.r-project.org/bin/windows/base/R-4.0.3-win.exe ( I guess you are in windows, in linux there's no escape from coding). R is really easy. I'm using library raster because it was faster than magick and imager libraries.


# a list of files; pattern is a string you want; 
# full.names parameter is to get the full name with directories
file_list = list.files(path = "/home/elio/Pictures/", pattern = "IMG", full.names = T)

dev.off()                             # to clean before plotting
par(mfrow = c(2,3))                   # 2 rows and 3 columns of plots 
for(i in file_list[1:6]) {            # for loop for the first 6 
temp = brick(i)                         # files are read, replaced at each loop
plotRGB(temp, main = basename(i),   margins = T)   # plotting function

enter image description here

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.