I used pymupdf successfully to carry out something similar. Details (and other approaches) in the thread of https://github.com/pymupdf/PyMuPDF/discussions/924#discussioncomment-7249686.
TLDR (copied my comment from over there):
Load doc and page (taken over from JorjMcKie's comment):
doc = fitz.open("input.pdf")
page = doc[pno] # read the page at page number pno
img_list = page.get_images(full=True) # a list of all images on that page
then:
p = fitz.Pixmap(doc, 6) # or whatever xref id
q = fitz.Pixmap(fitz.Colorspace(fitz.CS_RGB), p) # can save jpg only in RGB format, this was DeviceCMYK
q.save("6-rgb.jpg")
Now make whatever modding with Gimp, then load the modded back
r = fitz.Pixmap("6-rgb-mod.jpg")
s = fitz.Pixmap(fitz.Colorspace(fitz.CS_CMYK), r)
Aand now allegedly it would be as simple as
page.replace_image(6, pixmap=s)
but maybe I have an older pymupdf which was throwing an exception on missing doc.is_image (in newer source it is doc.xref_is_image, so probably fixed), so I followed the implementation of replace_image:
new_xref = page.insert_image(page.rect, pixmap=s)
doc.xref_copy(new_xref, 6)
last_contents_xref = page.get_contents()[-1]
doc.update_stream(last_contents_xref, b" ")
And finally save
doc.save("output.pdf", garbage=3, deflate=True)
Inspecting with mutool, the old image is still in place, but not used. So if you want to save space, probably this is not the good / full way. But if you want to replace an image quickly, leaving other visuals as-is (say for printing), then can be fine.
Doc reference: https://pymupdf.readthedocs.io/en/latest/