Skip to content

gwt-fetch

A research agent that opens a browser to save a register page is slow and leaves no trace of where the file came from. gwt-fetch downloads the file straight from the portal and writes one manifest.tsv row per file, so every image in your tree can be traced back to its source URL and checksum.

Python 3.11 or newer with requests. Run it as fetch/gwt-fetch, symlinked onto your PATH if you like, or install the package (pipx install gramps-web-toolkit, see Getting started) for gwt fetch and gwt-fetch. The code is in fetch/gwt_fetch/. Tests: cd fetch && python3 -m pytest.

iiif works with any IIIF archive in any country. digibib, gazeta and geo cover Spanish sources only. europeana searches every provider in any country; --provider Hispana narrows it to Spain.

gwt-fetch iiif URL [--pages 1-5,9] [--labels REGEX] [--native] [--out DIR] [--list]
gwt-fetch iiif --image-base URL/info.json [--pages 1-20] [--out DIR] [--list]
gwt-fetch crop IMG [...] (--box x,y,w,h [--from-size WxH] | --grid 2x3 [--overlap 10%] | --half left|right) [--max 1600] [--out DIR]
gwt-fetch digibib URL [--pages ...] [--out DIR] [--list]
gwt-fetch gazeta search (--text WORDS | --title WORDS) [--from YYYY-MM-DD] [--to YYYY-MM-DD] [--limit N]
gwt-fetch gazeta fetch BOE-A-YYYY-N [...] [--out DIR]
gwt-fetch europeana QUERY [--provider any|NAME] [--data-provider NAME] [--country NAME] [--year-from Y] [--year-to Y] [--iiif-only] [--rows N]
gwt-fetch geo PLACE [...] [--types Municipio,poblacion] [--province NAME] [--limit N]
gwt-fetch --delay 2 ...   # seconds between requests to one host, default 1
gwt-fetch iiif URL --out DIR --rawstore   # also digibib and gazeta fetch: images go to the bucket

What every download does

  • One row per file in DIR/manifest.tsv: file, label (the canvas label or document title), image_url (the exact URL that served the bytes), manifest_url (the manifest or document page), sha256, fetched_at (UTC).
  • It never overwrites. Files are written as *.part and renamed when complete, so a finished file on disk is always whole.
  • Re-running is resuming. The same command skips finished files. A finished file without a row (the run died between rename and append) gets its row on the next run. A file listed in the folder's rawstore.tsv counts as finished even when it is no longer on disk, so images moved to the bucket are not downloaded again.
  • --rawstore keeps images out of the working tree. After the run, every binary file gwt-fetch wrote in --out (images, PDF) is uploaded with gwt-rawstore and deleted locally once a HEAD confirms the object, its size and its sha256. Text files stay. A file the bucket does not confirm stays on disk, has no rawstore.tsv row, and the run exits 1; the next --rawstore run tries it again. The bucket settings are gwt-rawstore's, and a missing one stops the run before anything is downloaded.
  • Polite by default. One request per second per host, retries on 429 and 5xx with backoff, honours Retry-After. The user agent is Mozilla/5.0 (compatible; gwt-fetch/0.1; +https://github.com/GeiserX/gramps-web-toolkit); Antenati refuses agents that don't start with Mozilla/5.0.
  • Exit status. 0 when everything selected was fetched, 1 when some item failed (each failure is printed with the URLs tried and the HTTP status), 2 on a network or parse error.

Keep downloads out of the repo. Point --out at your research folder or /tmp.

iiif: any IIIF manifest, or a viewer URL it knows

Give it a IIIF Presentation manifest URL (v2 or v3), or a viewer URL from a portal below. It downloads each selected canvas through the Image API at full/max (the largest size the server allows), falling back to full/full on v2 servers and then to the static image named in the manifest.

--pages takes 1-based canvas positions (1-5,9,12-); --labels takes a regular expression matched against the canvas label (--labels '^pag\. (1|2)$'). --list prints position, label and URL and downloads nothing.

full/max is not always the native size: Antenati caps it at 10 megapixels. --native reads each canvas's info.json and asks for full/<width>,/0/default.jpg at the width it gives. The run checks the width of the image that comes back, and when the server sent another width (rescaled up or down) it saves nothing for that canvas, prints both widths and exits 1. Read names and dates at native width: a preview is for finding the act, not for reading it. A canvas already saved from full/max in the same --out folder is kept, and the native file goes beside it as NNNN_<label>_native.jpg; a canvas already saved at native width is skipped without a request. With --list, --native prints each canvas's info.json URL.

The cap is the server's own maxArea in info.json, and --native asks above it, on servers that already take seconds per image. Use it for the pages you will read, not for whole registers.

--image-base: an Image API with no manifest

Some viewers serve a IIIF Image API (.../info.json) and no Presentation manifest; French departmental archives are the common case. Their full/max and full/full can redirect to the HTML viewer, and a width above the native one comes back upscaled without an error. --image-base takes the image's base URL or its info.json and fetches it at native width, with the same width check as --native. Put {page} where the page number goes and give --pages with an end (1-20, not 12-), since there is no manifest to count the pages:

gwt-fetch iiif --image-base 'https://archives.example.fr/iiif/ark:/00000/abc/f{page}/info.json' --pages 10-12 --out /tmp/reg

Where the archive pads its page numbers (f_0007), write {page:04}. --pages without {page} in the URL, a range that selects nothing, and --labels stop with a message.

Each file is NNNN_<last path part>.jpg (0010_f10.jpg), and its row has the info.json URL as manifest_url. A page whose info.json fails is reported and the rest still download. Passing an info.json as the plain URL stops with a message that points here.

Portal Works Viewer URL to manifest
Portale Antenati (Italy) Yes, tested Paste the manifest URL, https://dam-antenati.cultura.gov.it/antenati/containers/5vj8KVP/manifest (the viewer's IIIF link). The unit page https://antenati.cultura.gov.it/ark:/12657/an_ua4076426/ also works where the portal answers, but its firewall refuses many addresses outside Italy. The manifest and image servers answer 403 unless the Referer is https://antenati.cultura.gov.it/; gwt-fetch sends that for every dam-antenati and iiif-antenati URL. full/full returns 403 and full/max is capped at 10 megapixels (maxArea in info.json), so a 4929×3781 scan arrives at 3610×2769; add --native for the 4929 px original.
Canadiana (Canada, LAC microfilm) Yes, tested https://heritage.canadiana.ca/view/{id}/{page} becomes https://heritage.canadiana.ca/iiif/{id}/manifest (Presentation 3). A reel's manifest is several megabytes. The site's robots.txt asks for 10 seconds between requests, so gwt-fetch waits at least that long on a Canadiana run; a longer --delay is kept.
Memòria Digital de Catalunya (and any CONTENTdm site) Yes, tested https://mdc.csuc.cat/digital/collection/{alias}/id/{id} becomes https://mdc.csuc.cat/iiif/2/{alias}:{id}/manifest.json. Images arrive at native size.
Europeana Yes, tested https://www.europeana.eu/{lang}/item/{collection}/{id} becomes https://iiif.europeana.eu/presentation/{collection}/{id}/manifest. When the provider has no Image API, Europeana's manifest points at its own thumbnail proxy (api.europeana.eu/thumbnail/v2/url.json?uri=...); gwt-fetch takes the original file from uri instead. The original can be a PDF.
Any Mirador or Universal Viewer link Yes A ?manifest= or ?iiif-content= parameter is used as the manifest.
Biblioteca Virtual de Prensa Histórica (BVPH) No IIIF; use digibib DIGIBIB viewer, no IIIF endpoint.
Biblioteca Virtual de Defensa No IIIF; use digibib DIGIBIB viewer, no IIIF endpoint.
Galiciana, Biblioteca Dixital de Galicia Needs a browser biblioteca.galiciana.gal answers record pages, viewer pages and image URLs with a JavaScript challenge page, also via Europeana's original-file links. gwt-fetch stops with a clear message; it does not solve the challenge.
Galiciana, Arquivo Dixital de Galicia Yes, through digibib or Europeana arquivo.galiciana.gal/arpadweb has no challenge; its single-file objects come back as PDF.
Biblioteca Digital Hispánica (BNE) Needs a browser bdh-rd.bne.es returns a 403 block page to scripted clients for viewer pages and for pdf.raw image URLs, from a datacenter address and from a home connection alike. No IIIF endpoint was found.

These archives serve plain manifests that gwt-fetch iiif reads as they are; build the URL from the item's id. Each template answered with a manifest in November 2026:

Archive Manifest URL
Riksarkivet (Sweden) https://lbiiif.riksarkivet.se/arkis!{bildid}/manifest
ANNO, Austrian National Library newspapers https://iiif.onb.ac.at/presentation/ANNO/{code}{yyyymmdd}/manifest/
Bayerische Staatsbibliothek (digitale-sammlungen) https://api.digitale-sammlungen.de/iiif/presentation/v2/{bsb_id}/manifest
National Diet Library (Japan) https://dl.ndl.go.jp/api/iiif/{pid}/manifest.json
Internet Archive https://iiif.archive.org/iiif/{identifier}/manifest.json

Antenati traps from real downloads (two Italian birth registers of the 1850s and 1860s):

  • The image server is slow: 10 to 60 seconds per image, about 20 on average. Five pages take close to two minutes, so run big selections in the background and let the resume logic carry a run that gets cut off.
  • A plain curl gets 403 from both antenati.cultura.gov.it (search and unit pages) and the image server. The user agent has to start with Mozilla/5.0, and image requests need the portal as Referer.
  • The web viewer shows about 1600 px and full/full returns a Cloudflare 403, which led one researcher to settle for 1600 px. full/max returns the largest size allowed: 3530×2716 for one page, 2122×1684 for another, depending on the scan. --native asks for full/<width>, and gets the scan's own width.
  • A canvas label is only pag. N. To find an act, list the canvases (--list), fetch a few pages around the estimate into /tmp, read the «Num. d'ordine» on each, then fetch just the right page into your research folder.

crop: cut a saved page into pieces

A register page is often too big to read in one look, and a double page holds two. crop cuts a saved image and writes each piece as a JPEG beside it:

  • --box x,y,w,h cuts one box, in the image's pixels. With --from-size WxH the box was measured on a preview of that size and is scaled to the real image. A box that reaches past the edge is refused.
  • --grid 2x3 cuts 2 columns by 3 rows. --overlap 10% makes each tile reach 10% of its size into its neighbours, so a line on a tile edge is whole in one of them.
  • --half left|right cuts the left or right half of a double page.
  • --max 1600 shrinks each piece so its longest side is at most 1600 px. Leave it out to keep native pixels.

--from-size without --box, and --overlap without --grid, are refused rather than ignored.

$ gwt-fetch crop /tmp/reg/0012_f12.jpg --grid 2x3 --overlap 10%
/tmp/reg/0012_f12_grid2x3-r1c1.jpg
...

Pieces are named <original>_box-x-y-w-h.jpg, <original>_grid2x3-r1c2.jpg or <original>_left.jpg. Each gets a row in the same folder's manifest.tsv, with the same columns as a download: label is crop x,y,w,h of <original> sha256:<original's sha256>, image_url is the original's URL from its own row (or its file name) with #xywh=x,y,w,h in the original's pixels, and manifest_url is the original's. Cropping a piece again keeps one #xywh= on the original: the new box is moved and scaled (after --max) into the original's pixels. Like a download, a piece is never overwritten, and re-running skips finished ones. If that folder's manifest.tsv was written by another tool (other columns), crop refuses to add to it; --out DIR writes the pieces and their rows to another folder, and image_url then names the original by its path from there.

crop needs Pillow: pipx install 'gramps-web-toolkit[crop]', or pip install Pillow beside a clone. Without it, crop prints how to install it and exits 2.

digibib: DIGIBIB viewers without IIIF

DIGIBÍS's DIGIBIB runs BVPH, the Biblioteca Virtual de Defensa, Galiciana and many regional digital libraries. None of these installs serve IIIF. The viewer URL is .../catalogo_imagenes/grupo.do?path=N. Each page view (&presentacion=pagina&posicion=K) names one image as imagen_id.do?idImagen=M, and that URL with &formato=jpg returns the full JPEG. gwt-fetch digibib walks those page views. When the object is a single file, grupo.do serves the file itself (often a PDF); it is saved as 0001_document.pdf.

Tested: Biblioteca Virtual de Defensa (grupo.do?path=359360, 17 pages, 3764×2443 JPEGs), BVPH (grupo.do?path=1000178404, 72 pages), Galiciana Arquivo Dixital (grupo.do?path=264703, one PDF).

prensahistorica.mcu.es and the other *.mcu.es sites return HTTP 429 to some egress addresses (seen from a corporate VPN exit) on every request, while the same request from a home connection works. If every request gets 429, change network rather than raising --delay.

gazeta: the historical Gaceta de Madrid

The BOE open data API (/datosabiertos/api/) covers consolidated legislation and the modern BOE and BORME summaries only, with no endpoint for the «Gazeta» colección histórica (1661-1959). gwt-fetch therefore uses the public pages, whose URLs are stable:

  • Search: GET https://www.boe.es/buscar/gazeta.php with the form's own fields as query parameters. Each field is a campo[i]/dato[i] pair, joined by operador[i] (and, or, not):
Field campo[i] dato[i] gwt-fetch flag
Full text campo[4]=DOC words; wrap a phrase in double quotes --text
Title campo[1]=TITULOS words --title
Gazette title campo[6]=GAZ.ID 7 = Gaceta de Madrid, 4 = Gaceta de la República, 8 = BOE, and 12 more in the form --gazette
Publication date campo[8]=FPU dato[8][0] and dato[8][1], YYYY-MM-DD, 1661-01-01 to 1959-12-31 --from, --to
Also campo[0]=ID_HIST, campo[2]=RNG.ID, campo[3]=DEM.ID, campo[7]=NBO rank, department, issue number (n/aaaa) not exposed

Plus page_hits (only 50, 200, 500, 1000 or 2000), sort_field[0]=FPU, sort_order[0]=asc and accion=Buscar. Results are <li class="resultado-busqueda"> blocks with the title, «núm. N, de DD/MM/AAAA, página(s) P», the department and the item PDF with its reference. The output is TSV: ref, date, gazette, number, page, department, title, pdf_url, issue, issue_pdf. The site returns at most 2000 hits per search; narrow the dates for more. - Whole issue: for the Gaceta de Madrid, issue is GMD-{year}-{number} and issue_pdf is https://www.boe.es/gazeta/dias/{YYYY}/{MM}/{DD}/pdfs/GMD-{year}-{number}.pdf, with the number unpadded (GMD-1870-40, not -040). - Document by reference: https://www.boe.es/buscar/doc.php?id=BOE-A-1870-1111 (title, «Publicado en», a link to the PDF). - PDF: https://www.boe.es/datos/pdfs/BOE//{year}/{issue:03}/A{first:05}-{last:05}.pdf, the scanned page or pages of the original issue.

Traps we hit with real searches:

  • --text Surname1 Surname2 matches the two words anywhere in the item, so a two-surname search returns several times more hits than it should, mostly noise. --text '"Surname1 Surname2"' is the phrase, and returns the same hits the BOE search form returns in a browser.
  • References are not all BOE-A-: BOE-I-1892-51060 and BOE-B-1957-12194 came back from the same search. gazeta fetch takes any BOE-<letter>-<year>-<n>.
  • An item's title can hide what matters. BOE-A-1864-10018 is titled «Gobierno de la provincia de Zaragoza. Registro de la Propiedad de Yeste.», yet its two pages are a long list of farms in Moratalla and Nerpio with every owner's name. The full-text search finds such names; the title never would. Read the PDF before you call a hit a homonym.
  • The search does not index the annexes «Anuncios, avisos y Administración de Justicia» of many years. For those, download the whole issue (issue_pdf) and search its text.

To rank the acts that name one person as gwt-rank mentions, with a verdict from the birth year and the parents, use gwt-read boe search.

gazeta fetch REF saves REF.pdf and REF_text.txt. The text is the HTML text when the BOE has transcribed it. Otherwise it is the PDF's own OCR layer through pdftotext (poppler), when that is installed. That OCR is rough on 19th-century type but good enough to search.

$ gwt-fetch gazeta search --text Betanzos --from 1890-01-01 --to 1890-03-31 --limit 3
ref date    gazette number  page    department  title   pdf_url
BOE-A-1890-676  1890-02-02  Gaceta de Madrid    33  325 Ministerio de la Gobernación    Estación Central de Telégrafos.- Telegramas recibidos ...   https://www.boe.es/datos/pdfs/BOE//1890/033/A00325-00325.pdf
$ gwt-fetch gazeta fetch BOE-A-1890-676 --out research/raw/fuentes/gaceta

europeana: Spanish records with a manifest

The Europeana Search API needs a free key: get one at https://pro.europeana.eu/pages/get-api and export EUROPEANA_KEY=.... Without a key, the command prints a message and exits 0, so pipelines carry on.

By default it searches every provider. --provider NAME filters to one aggregator, for example --provider Hispana for Spain (4.5 million records: BVPH, Galiciana, BNE, the regional digital libraries and more). Use --data-provider to narrow to one institution as Europeana names it, for example "Galiciana: Digital Library of Galicia" or "Virtual Library of Historical Press". --country spain keeps records whose Europeana country is that one, whoever provided them. Output is TSV with a manifest column for each record, which gwt-fetch iiif can fetch.

--iiif-only adds sv_dcterms_conformsTo:*iiif*, keeping only records whose provider serves a real Image API. In October 2026 that returns zero Hispana records: no Spanish provider exposes IIIF through Europeana, while France, Austria and the UK expose millions. For Spanish records, use the Europeana manifest without --iiif-only; iiif swaps each thumbnail for the provider's original file. Where that provider blocks scripted clients (Galiciana's library, BNE), you still need the browser.

geo: Spanish places to coordinates and INE code

CartoCiudad's geocoder (IGN/CNIG, https://www.cartociudad.es/geocoder/api/geocoder/candidates then /find) turns a place name into coordinates, and every result carries the INE municipality code (muniCode, five digits: province plus municipality). No key needed.

$ gwt-fetch geo Betanzos Ortigueira --province "A Coruña" --limit 1
query   type    name    municipality    ine_code    province    lat lng cartociudad_id
Betanzos    poblacion   Betanzos    Betanzos    15009   A Coruña    43.278535   -8.213380   1200023058
Ortigueira  poblacion   Ortigueira  Ortigueira  15061   A Coruña    43.684475   -7.850976   1200024424

--types picks CartoCiudad result types: Municipio (the municipality, centroid), poblacion (a settlement), toponimo (named places such as churches and parish cemeteries, useful for parishes) and callejero (streets). Historical names that no longer exist won't match; geocode the modern municipality and keep the historical name in your own record.

Spanish open-data projects we looked at

From the awesome-spain list (https://github.com/GeiserX/awesome-spain):

  • CartoCiudad / pycartociudad / caRtociudad: used. geo calls the same public API directly, so it needs no extra dependency.
  • INE municipality codes (municipios-provincias-comunidades-INE-spain, ccaa-provincias-municipios): not needed, because CartoCiudad already returns the INE code with each match.
  • spanish-names-surnames (INE name and surname frequencies): not a fetch source. It may help a name-normalising step later.
  • boe-mcp, MCP-BOE, rOpenSpain/BOE, Datania BOE: they wrap the modern BOE open data API, which does not cover the Gazeta collection. gazeta is the part that does.
  • API-CNIG, mapSpain, es-atlas: map display and boundaries, outside what fetch does.