Skip to main content

XCF Sync — GIMP XCF → GitHub Auto-Sync

xcf-git-sync — Watch .xcf files, export only the layers you need as PNG, and auto-push.

PyPI License: MIT Python 3.9+

Built for large worldbuilding maps and multi-layer XCFs. GIMP is great for drawing world maps, and Git is great for versioning them — but a GIMP .xcf is a big binary blob you can't diff, and your maps app can't read it. XCF Sync watches your XCF and syncs only the tagged layers you want (like [GH] Borders, [GH] Rivers) as PNGs. Push the XCF, and the PNGs update themselves.

How it works

your.xcf --(watch)--> filter layers --> PNGs in assets/xcf_layers/ --> git commit/push
  • GIMP 2.10 files (gimp xcf v011 and earlier) are parsed directly with gimpformats — no GIMP install needed.
  • GIMP 3 files (v012+, e.g. v019) are exported by driving headless GIMP.
  • One XCF maps to one output folder; re-exports overwrite their own PNGs.

Features

  • Selective export by tag, group, name, or regex.
  • GIMP 2.10 and GIMP 3 support — fast gimpformats path for v011, headless GIMP fallback for v012+.
  • Large-map friendly — each layer is written as its own PNG instead of one giant composite, so pipelines can consume layers individually.
  • Watch mode — instant local export when you save in GIMP.
  • Git auto-sync — auto-commit and push changed layers.
  • Cloud safety net — a GitHub Actions workflow that syncs even when no one is running the watcher locally.

Requirements

  • Python 3.9+
  • Git
  • GIMP 2.10+ only if you use GIMP 3 XCFs (auto-detected, or forced with --via-gimp). Auto-detect checks PATH, C:\Program Files\GIMP *, %LOCALAPPDATA%\Programs\GIMP *, and Flatpak.

Install

pip install xcf-git-sync
# or install it isolated so it doesn't touch your project environment:
pipx install xcf-git-sync

Verify:

xcf-git-sync --version

To work on the tool itself, install from a clone instead:

git clone https://github.com/EmjayBot/xcf-git-sync.git
cd xcf-git-sync
pip install -e ".[dev]"

Your first export

Work from your art repo (the folder with .git in it).

1. Inspect the layer tree. Before exporting, see every layer and preview which ones your filters would keep:

xcf-git-sync --xcf maps/source/urth.xcf --list-layers

Each line is Group/Sub/Layer, with (hidden) and [keep] marks. This step uses headless GIMP, so GIMP must be installed even for a GIMP 2.10 file.

2. Export once. --once exports and exits without watching:

xcf-git-sync --xcf maps/source/urth.xcf --repo . --once

PNGs land in assets/xcf_layers/urth/ by default. Nothing is committed yet, so open them and check the result.

3. Keep only the layers you want. XCFs usually contain sketches, references, and UI layers you don't want to ship. Combine filters (all comma-separated):

xcf-git-sync --xcf maps/source/urth.xcf --repo . --once \
  --include "National,Cities,Sub-National,Political,Ocean" \
  --exclude "Sketch,WIP"

4. Watch and auto-push. Drop --once to keep running:

xcf-git-sync --xcf maps/source/urth.xcf --repo . --push

Now every time GIMP saves the XCF, the matching layers are re-exported, committed, and pushed. Leave it running while you draw; press Ctrl+C to stop. To cover a whole folder (recursively), use --watch-dir ./maps/source instead of --xcf.

Output layout

For maps/source/urth.xcf with the default output root:

assets/xcf_layers/
└── urth/
    ├── National.png
    ├── Cities.png
    ├── Political/
    │   └── Borders.png
    └── _flattened.png
  • The folder is the XCF's filename (slugified), so two XCFs never collide.
  • Layer groups become subfolders.
  • Names are slugified: characters that aren't letters, digits, _, -, or space are dropped, runs of spaces become -, and names are capped at 100 chars. National #2 → National-2.png.
  • If two different layers slugify to the same name in one folder, the tool adds -2, -3, and so on. A re-export overwrites the file it made before.
  • _flattened.png is the merged visible composite. Skip it with --no-flatten.

Filtering layers

Filters are applied in this order, and --include* only kicks in when you set it (otherwise everything visible is eligible):

Flag Config key Effect
--tag-prefix "[GH]" tag_prefix keep only layers whose name starts with the prefix
--group "Sprites,UI" group keep only layers whose group path starts with or contains one of these
--include "Hero,Logo" include if set, keep only layers whose name or full Group/Layer path contains one of these substrings (case-insensitive)
--include-regex ".*_export$" include_regex same idea, but each entry is a Python regex (re.search, case-sensitive)
--exclude "Sketch,WIP" exclude drop layers matching these substrings (case-insensitive)
--exclude-regex "^temp" exclude_regex drop layers matching these regexes
--no-only-visible only_visible: false include hidden layers (default is visible only)

Notes:

  • Order is: visibility → tag prefix → group → include → exclude. Exclude always wins.
  • --include is a substring match, not exact: Hero also matches HeroBackup. Use --include-regex "^Hero$" for exact names.
  • Multiple values are comma-separated, including regexes: --include-regex ".*_export$,^UI_".

Always confirm with --list-layers plus the same filter flags before a long run.

Using a config file

Copy config.example.yaml to config.yaml (it's git-ignored) and run with --config. CLI flags override the file.

watch:
  xcf: ./maps/source/urth.xcf     # or watch_dir: ./maps/source
  # watch_dir: ./art
  repo: ./
  out: ./assets/xcf_layers
  push: true                      # auto_push: true is an alias
  only_visible: true
  no_flatten: false
  debounce: 1.5
  tag_prefix: ""
  include: "National,Cities,Political,Ocean"
  exclude: "Sketch,WIP"
  # include_regex: ".*_export$"
  # exclude_regex: "^temp"
  # group: "Sprites,UI"
  via_gimp: false
  timeout: 300
  # gimp_bin: "flatpak run org.gimp.GIMP"
xcf-git-sync --config config.yaml

xcf or watch_dir is required. The github: section in the example (branch, lfs) is reserved and not wired up yet.

GIMP 3 support

The tool reads the XCF header to decide:

  • v011 and earlier → parsed directly with gimpformats (fast, no GIMP needed).
  • v012+ (GIMP 3, e.g. v019) → exported via headless GIMP with --batch-interpreter=python-fu-eval. Verified on GIMP 3.2.6 and 2.10.32.

Force the GIMP path for any file (useful for testing) with --via-gimp:

xcf-git-sync --xcf design.xcf --repo . --once --via-gimp

If GIMP isn't found, or lives somewhere unusual, point at it explicitly. The value may be a full path or a compound command:

xcf-git-sync --xcf design.xcf --repo . --once \
  --gimp-bin "flatpak run org.gimp.GIMP"

The environment variable XCF_GIT_SYNC_GIMP_BIN does the same.

Auto-sync in the cloud (GitHub Actions)

The local watcher is instant, but it only runs while someone runs it. As a safety net, run the export in CI: any push touching the XCF regenerates the PNGs and commits them back.

  1. Copy examples/urthmaps-sync.yaml into your art repo as .github/workflows/xcf-sync.yaml.
  2. Adjust XCF_PATH, FILTERS, and branch names at the top.
  3. Push an XCF change — the PNGs update themselves a few minutes later.

It installs headless GIMP 3 from the official Flatpak, so even GIMP 3 XCFs work. The paths: trigger means the bot's own PNG commit never re-triggers the workflow (no loops). Free on public repos.

Why not Cloudflare? Workers can't run GIMP — no custom native binaries, tight CPU/memory limits, no GTK stack. The compute has to live where GIMP can run (GitHub runners, a VM, or a container).

Command reference

Flag Description
--xcf PATH Single XCF to export / watch.
--watch-dir DIR Export all *.xcf under DIR, then watch it recursively.
--repo DIR Git repo root (default .). Only files inside it are committed.
--out DIR PNG output root. Default <repo>/assets/xcf_layers.
--once Export once and exit (no watcher).
--push git push after each commit.
--config FILE Load a YAML config file (CLI flags override it).
--list-layers Print the layer tree (via headless GIMP) and exit.
--tag-prefix S Keep only layers whose name starts with S.
--include LIST Comma-separated substrings to include.
--include-regex LIST Comma-separated regexes to include.
--exclude LIST Comma-separated substrings to exclude.
--exclude-regex LIST Comma-separated regexes to exclude.
--group LIST Only layers inside these groups.
--no-only-visible Include hidden layers.
--no-flatten Skip _flattened.png.
--debounce SEC Seconds to ignore rapid saves (default 1.5).
--via-gimp Always export via headless GIMP.
--gimp-bin CMD GIMP binary/command override.
--timeout SEC Headless GIMP timeout (default 300).
--version Print the version.

Troubleshooting

  • needs headless GIMP to export, but no GIMP binary was found — install GIMP 2.10+ / GIMP 3, or pass --gimp-bin.
  • GIMP batch export produced nothing — run --list-layers first to confirm GIMP can open the file. The last 15 lines of GIMP output are printed to help.
  • Timeout on the first run — GIMP's first launch builds caches. Retry, or raise --timeout.
  • is not a git repo, skipping commit — --repo points at the wrong folder.
  • Push failed — check the remote and credentials. On Windows, Git Credential Manager is the usual fix; on CI the contents: write permission and GITHUB_TOKEN do it.
  • A hidden layer didn't export — hidden layers are skipped by default; add --no-only-visible.
  • A GIMP 3 file fails with no GIMP installed — gimpformats can't read v012+ XCFs, so GIMP 3 files always need headless GIMP. Install GIMP or point --gimp-bin at it.

Security notes

  • Only open XCF files you trust. The whole file is parsed (by gimpformats or, for GIMP 3 files, inside headless GIMP). A crafted XCF can crash the tool or target a parser bug, so don't point it at files from untrusted sources.
  • Your environment and config are trusted input. XCF_GIT_SYNC_GIMP_BIN chooses which GIMP binary runs, and the batch job is exec'd inside GIMP. Run the tool only with an environment you control — not a shared runner or CI job fed by strangers.
  • The example workflow is pinned. It installs xcf-git-sync at a fixed version and its actions at fixed commit SHAs because the job holds contents: write. If you copy it, keep the pins and bump them deliberately.
  • Layer/group names are sanitized with slugify and git arguments are passed as argv after --, so an XCF's contents can't inject shell or git options.

Releasing (maintainers)

Publishing runs in .github/workflows/release.yml using PyPI Trusted Publishing (OIDC), so there is no API token to store or leak.

One-time setup on PyPI: add a trusted publisher for the xcf-git-sync project with owner EmjayBot, repository xcf-git-sync, workflow release.yml, and environment pypi.

To cut a release, bump the version in both pyproject.toml ([project].version) and src/xcf_git_sync/__init__.py (__version__), then tag it:

git commit -am "release: v0.1.1"
git tag v0.1.1
git push origin main --tags

The tag triggers the workflow, which verifies the tag matches pyproject.toml, builds the sdist + wheel, and publishes to PyPI. workflow_dispatch is enabled for manual runs.

Legacy scripts

legacy/xcf_git_sync.py and legacy/xcf_git_sync_selective.py are the original prototypes (built for gimpformats==1.1.3; current gimpformats>=2025 changed its API). They are kept for reference — the supported code is the xcf_git_sync package (src/xcf_git_sync/) with the xcf-git-sync command.

Contribute

pip install -e ".[dev]"
pytest -q

PRs welcome: GIMP 3 batch export hardening, tests with more XCF fixtures, tray app.

Release files for xcf-git-sync 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for xcf-git-sync 0.1.0
File Size Uploaded
xcf_git_sync-0.1.0.tar.gz 28.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for xcf-git-sync 0.1.0
File Interpreter ABI Platform
xcf_git_sync-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 51.5 kB

Release files / xcf_git_sync-0.1.0.tar.gz

Download URL xcf_git_sync-0.1.0.tar.gz
Size 28.9 kB
Tags Source
SHA-256 checksum
How to use checksums
2fb0993f874a7844f038337938893c8aed7512674515dc8052df2e949c2f4a6b
BLAKE2b-256 checksum
How to use checksums
5d48d662d776964996e233fe7dcfe6a385665a6ea904974769c75ff96babfeaf
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 26, 2026.

Transparency log

Release files / xcf_git_sync-0.1.0-py3-none-any.whl

Download URL xcf_git_sync-0.1.0-py3-none-any.whl
Size 22.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
846d64916a48864c6e9f708f828e8052a79edb1e3536d206b228b4ffb6cced85
BLAKE2b-256 checksum
How to use checksums
34e80d430f3246f5a689d2098e4e52c8ffa5cf4abcb6ee097f50b058d7ab701e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 26, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page