├── tests ├── __init__.py ├── perf │ ├── .gitignore │ ├── images │ │ └── sample2.v │ ├── operation-call.py │ ├── vips-bench.py │ ├── README.rst │ └── run.sh ├── images │ ├── sample.jpg │ ├── sample.webp │ ├── ultra-hdr.jpg │ └── logo.svg ├── conftest.py ├── test_block.py ├── test_saveload.py ├── test_progress.py ├── test_gvalue.py ├── test_constants.py ├── helpers │ └── helpers.py ├── test_connections.py └── test_image.py ├── doc ├── README.rst ├── global.rst ├── vsource.rst ├── vtarget.rst ├── vobject.rst ├── voperation.rst ├── vconnection.rst ├── vinterpolate.rst ├── vsourcecustom.rst ├── vtargetcustom.rst ├── error.rst ├── gobject.rst ├── base.rst ├── _templates │ └── layout.html ├── gvalue.rst ├── enums.rst ├── _static │ ├── collapse.js │ └── style.css ├── index.rst ├── conf.py └── intro.rst ├── MANIFEST.in ├── examples ├── try13.py ├── try9.py ├── affine.py ├── try3.py ├── try6.py ├── stdio.py ├── try10.py ├── try17.py ├── try11.py ├── try8.py ├── soak-test.py ├── orientation.py ├── read_profile.py ├── try7.py ├── try1.py ├── try14.py ├── try16.py ├── try4.py ├── try12.py ├── split-animation.py ├── watermark_image.py ├── try2.py ├── join-animation.py ├── stream.py ├── convolve.py ├── annotate-animation.py ├── thumb-dir.py ├── watermark.py ├── progress.py ├── pil-numpy-pyvips.py ├── shepard.py ├── cod.py ├── try5.py ├── connection.py ├── watermark_context.py └── gen-enums.py ├── pyvips ├── version.py ├── vinterpolate.py ├── vconnection.py ├── pyvips_build.py ├── vregion.py ├── error.py ├── vsourcecustom.py ├── vtarget.py ├── vsource.py ├── vtargetcustom.py ├── vobject.py ├── base.py ├── __init__.py ├── gobject.py ├── gvalue.py └── vdecls.py ├── .gitignore ├── tox.ini ├── setup.py ├── LICENSE.txt ├── .github └── workflows │ └── ci.yml ├── pyproject.toml ├── README.rst └── CHANGELOG.rst /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/README.rst: -------------------------------------------------------------------------------- 1 | ../README.rst -------------------------------------------------------------------------------- /doc/global.rst: -------------------------------------------------------------------------------- 1 | .. py:module:: pyvips 2 | -------------------------------------------------------------------------------- /tests/perf/.gitignore: -------------------------------------------------------------------------------- 1 | tmp 2 | *.json 3 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | # Include the license file 2 | include LICENSE.txt 3 | -------------------------------------------------------------------------------- /tests/images/sample.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/libvips/pyvips/HEAD/tests/images/sample.jpg -------------------------------------------------------------------------------- /tests/images/sample.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/libvips/pyvips/HEAD/tests/images/sample.webp -------------------------------------------------------------------------------- /tests/images/ultra-hdr.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/libvips/pyvips/HEAD/tests/images/ultra-hdr.jpg -------------------------------------------------------------------------------- /tests/perf/images/sample2.v: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/libvips/pyvips/HEAD/tests/perf/images/sample2.v -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | 4 | sys.path.append(os.path.join(os.path.dirname(__file__), 'helpers')) 5 | -------------------------------------------------------------------------------- /examples/try13.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # import pyvips 4 | import logging 5 | 6 | logging.basicConfig(level=logging.DEBUG) 7 | -------------------------------------------------------------------------------- /doc/vsource.rst: -------------------------------------------------------------------------------- 1 | .. include global.rst 2 | 3 | ``Source`` 4 | ========== 5 | 6 | .. automodule:: pyvips.vsource 7 | :members: 8 | -------------------------------------------------------------------------------- /doc/vtarget.rst: -------------------------------------------------------------------------------- 1 | .. include global.rst 2 | 3 | ``Target`` 4 | ========== 5 | 6 | .. automodule:: pyvips.vtarget 7 | :members: 8 | -------------------------------------------------------------------------------- /doc/vobject.rst: -------------------------------------------------------------------------------- 1 | .. include global.rst 2 | 3 | ``VipsObject`` 4 | ============== 5 | 6 | .. automodule:: pyvips.vobject 7 | :members: 8 | -------------------------------------------------------------------------------- /pyvips/version.py: -------------------------------------------------------------------------------- 1 | # this is used in pyproject.toml and imported into __init__.py 2 | __version__ = '3.1.1' 3 | 4 | __all__ = ['__version__'] 5 | -------------------------------------------------------------------------------- /doc/voperation.rst: -------------------------------------------------------------------------------- 1 | .. include global.rst 2 | 3 | ``Operation`` 4 | ============= 5 | 6 | .. automodule:: pyvips.voperation 7 | :members: 8 | -------------------------------------------------------------------------------- /doc/vconnection.rst: -------------------------------------------------------------------------------- 1 | .. include global.rst 2 | 3 | ``Connection`` 4 | ============== 5 | 6 | .. automodule:: pyvips.vconnection 7 | :members: 8 | -------------------------------------------------------------------------------- /doc/vinterpolate.rst: -------------------------------------------------------------------------------- 1 | .. include global.rst 2 | 3 | ``Interpolate`` 4 | =============== 5 | 6 | .. automodule:: pyvips.vinterpolate 7 | :members: 8 | -------------------------------------------------------------------------------- /doc/vsourcecustom.rst: -------------------------------------------------------------------------------- 1 | .. include global.rst 2 | 3 | ``SourceCustom`` 4 | ================ 5 | 6 | .. automodule:: pyvips.vsourcecustom 7 | :members: 8 | -------------------------------------------------------------------------------- /doc/vtargetcustom.rst: -------------------------------------------------------------------------------- 1 | .. include global.rst 2 | 3 | ``TargetCustom`` 4 | ================ 5 | 6 | .. automodule:: pyvips.vtargetcustom 7 | :members: 8 | -------------------------------------------------------------------------------- /doc/error.rst: -------------------------------------------------------------------------------- 1 | .. include global.rst 2 | 3 | ``Error`` 4 | ========= 5 | 6 | Errors from libvips. 7 | 8 | .. automodule:: pyvips.error 9 | :members: 10 | -------------------------------------------------------------------------------- /doc/gobject.rst: -------------------------------------------------------------------------------- 1 | .. include global.rst 2 | 3 | ``GObject`` 4 | =========== 5 | 6 | .. automodule:: pyvips.gobject 7 | :members: 8 | :special-members: 9 | -------------------------------------------------------------------------------- /doc/base.rst: -------------------------------------------------------------------------------- 1 | .. include global.rst 2 | 3 | Base definitions 4 | ================ 5 | 6 | Basic utility stuff. 7 | 8 | .. automodule:: pyvips.base 9 | :members: 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .pytest_cache 2 | .*.swp 3 | .DS_Store 4 | *.pyc 5 | *.egg-info 6 | *.egg 7 | *.eggs 8 | *.cache 9 | _libvips.* 10 | dist 11 | doc/build 12 | build 13 | .tox 14 | tmp 15 | -------------------------------------------------------------------------------- /examples/try9.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import logging 4 | import pyvips 5 | 6 | logging.basicConfig(level=logging.DEBUG) 7 | a = pyvips.Image.black(100, 100) 8 | a.write_to_file("x.v") 9 | -------------------------------------------------------------------------------- /doc/_templates/layout.html: -------------------------------------------------------------------------------- 1 | {% extends "!layout.html" %} 2 | {% set script_files = script_files + [pathto("_static/collapse.js", True)] %} 3 | {% block extrahead %} 4 | 5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /doc/gvalue.rst: -------------------------------------------------------------------------------- 1 | .. include global.rst 2 | 3 | ``GValue`` 4 | ========== 5 | 6 | This module defines the pyvips ``GValue`` class. You can use instances of 7 | this class to get and set :class:`.GObject` properties. 8 | 9 | .. automodule:: pyvips.gvalue 10 | :members: 11 | 12 | -------------------------------------------------------------------------------- /examples/affine.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import sys 4 | import pyvips 5 | 6 | x = pyvips.Image.new_from_file(sys.argv[1]) 7 | if not x.hasalpha(): 8 | x = x.addalpha() 9 | y = x.affine([0.70710678, 0.70710678, -0.70710678, 0.70710678]) 10 | y.write_to_file(sys.argv[2]) 11 | -------------------------------------------------------------------------------- /examples/try3.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import sys 4 | import pyvips 5 | 6 | # import logging 7 | # logging.basicConfig(level = logging.DEBUG) 8 | 9 | pyvips.cache_set_trace(True) 10 | 11 | a = pyvips.Image.new_from_file(sys.argv[1]) 12 | print(a.max()) 13 | print(a.max()) 14 | -------------------------------------------------------------------------------- /examples/try6.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import sys 4 | import pyvips 5 | 6 | # import logging 7 | # logging.basicConfig(level = logging.DEBUG) 8 | 9 | a = pyvips.Image.new_from_file(sys.argv[1]) 10 | 11 | b = a.write_to_buffer(".jpg") 12 | 13 | c = pyvips.Image.new_from_buffer(b, "") 14 | -------------------------------------------------------------------------------- /examples/stdio.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import sys 4 | import pyvips 5 | 6 | source = pyvips.Source.new_from_descriptor(sys.stdin.fileno()) 7 | image = pyvips.Image.new_from_source(source, "") 8 | target = pyvips.Target.new_to_descriptor(sys.stdout.fileno()) 9 | image.write_to_target(target, ".jpg") 10 | -------------------------------------------------------------------------------- /examples/try10.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # import logging 4 | # logging.basicConfig(level = logging.DEBUG) 5 | 6 | import pyvips 7 | 8 | a = pyvips.Image.black(100, 100) 9 | a = a.draw_circle(128, 50, 50, 20) 10 | b = a.hough_circle(scale=1, min_radius=15, max_radius=25) 11 | b.write_to_file("x.v") 12 | -------------------------------------------------------------------------------- /examples/try17.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import sys 4 | 5 | import pyvips 6 | 7 | # import logging 8 | # logging.basicConfig(level = logging.DEBUG) 9 | 10 | # pyvips.cache_set_trace(True) 11 | 12 | a = pyvips.Image.new_from_file(sys.argv[1]) 13 | 14 | a = a[1:] 15 | 16 | a.write_to_file(sys.argv[2]) 17 | -------------------------------------------------------------------------------- /examples/try11.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import sys 4 | import pyvips 5 | 6 | # import logging 7 | # logging.basicConfig(level = logging.DEBUG) 8 | 9 | a = pyvips.Image.new_from_file(sys.argv[1]) 10 | ipct = a.get("ipct-data") 11 | print("ipct = ", ipct.get()) 12 | a.remove("ipct-data") 13 | a.write_to_file("x.jpg") 14 | -------------------------------------------------------------------------------- /examples/try8.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # import logging 4 | # logging.basicConfig(level = logging.DEBUG) 5 | 6 | import pyvips 7 | 8 | a = pyvips.Image.new_from_array([[1, 2, 3], [4, 5, 6], [7, 8, 9]], 8, 128) 9 | 10 | print('scale =', a.get('scale')) 11 | print('offset =', a.get('offset')) 12 | 13 | a.write_to_file("x.v") 14 | -------------------------------------------------------------------------------- /doc/enums.rst: -------------------------------------------------------------------------------- 1 | .. include global.rst 2 | 3 | The libvips enums 4 | ================= 5 | 6 | This module contains the various libvips enums as Python classes. 7 | 8 | Enums values are represented in pyvips as strings. These classes contain the 9 | valid strings for each enum. 10 | 11 | .. automodule:: pyvips.enums 12 | :members: 13 | -------------------------------------------------------------------------------- /examples/soak-test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import sys 4 | import pyvips 5 | 6 | pyvips.leak_set(True) 7 | pyvips.cache_set_max(0) 8 | 9 | for i in range(1000): 10 | print(f"loop {i} ...") 11 | im = pyvips.Image.new_from_file(sys.argv[1]) 12 | im = im.embed(100, 100, 3000, 3000, extend="mirror") 13 | im.write_to_file("x.v") 14 | -------------------------------------------------------------------------------- /examples/orientation.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import sys 4 | import pyvips 5 | 6 | a = pyvips.Image.new_from_file(sys.argv[1]) 7 | 8 | try: 9 | orientation = a.get('exif-ifd0-Orientation') 10 | a.set('orientation', int(orientation.split()[0])) 11 | except Exception: 12 | a.set('orientation', 0) 13 | 14 | a.write_to_file(sys.argv[2]) 15 | -------------------------------------------------------------------------------- /examples/read_profile.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import sys 4 | import pyvips 5 | 6 | # import logging 7 | # logging.basicConfig(level = logging.DEBUG) 8 | # pyvips.cache_set_trace(True) 9 | 10 | a = pyvips.Image.new_from_file(sys.argv[1]) 11 | 12 | profile = a.get("icc-profile-data") 13 | 14 | with open('x.icm', 'w') as f: 15 | f.write(profile) 16 | -------------------------------------------------------------------------------- /examples/try7.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import sys 4 | import pyvips 5 | 6 | # import logging 7 | # logging.basicConfig(level = logging.DEBUG) 8 | 9 | a = pyvips.Image.new_from_file(sys.argv[1]) 10 | 11 | b = a.write_to_memory() 12 | 13 | c = pyvips.Image.new_from_memory(b, a.width, a.height, a.bands, a.bandfmt) 14 | 15 | c.write_to_file("x.v") 16 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | skipdist = true 3 | 4 | envlist = py{310,311,312,313,314,py39}-test 5 | 6 | [pytest] 7 | norecursedirs = .eggs build tmp* vips-* 8 | log_level = WARNING 9 | 10 | [testenv] 11 | extras = test 12 | commands = pytest 13 | passenv = PKG_CONFIG_PATH 14 | 15 | [testenv:doc] 16 | extras = doc 17 | commands = sphinx-build -n -b html doc doc/build/html 18 | -------------------------------------------------------------------------------- /examples/try1.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import sys 4 | 5 | # uncomment to see startup log for pyvips 6 | # import logging 7 | # logging.basicConfig(level=logging.DEBUG) 8 | 9 | import pyvips 10 | 11 | 12 | print('test Image') 13 | image = pyvips.Image.new_from_file(sys.argv[1]) 14 | print('image =', image) 15 | print('image.width =', image.width) 16 | print('\n''') 17 | -------------------------------------------------------------------------------- /examples/try14.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # import logging 4 | # logging.basicConfig(level = logging.DEBUG) 5 | 6 | import pyvips 7 | 8 | a = pyvips.Image.black(100, 100) 9 | b = a.bandjoin(2) 10 | 11 | b.write_to_file("x.v") 12 | 13 | txt = pyvips.Image.text("left corner", dpi=300) 14 | 15 | c = txt.ifthenelse(2, [0, 255, 0], blend=True) 16 | 17 | c.write_to_file("x2.v") 18 | -------------------------------------------------------------------------------- /examples/try16.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import logging 4 | import sys 5 | 6 | import pyvips 7 | 8 | logging.basicConfig(level=logging.DEBUG) 9 | 10 | # pyvips.cache_set_trace(True) 11 | 12 | a = pyvips.Image.new_from_file(sys.argv[1]) 13 | 14 | x = a.erode([[128, 255, 128], 15 | [255, 255, 255], 16 | [128, 255, 128]]) 17 | 18 | x.write_to_file(sys.argv[2]) 19 | -------------------------------------------------------------------------------- /tests/perf/operation-call.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import pyperf 3 | import pyvips 4 | 5 | 6 | def operation_call(loops): 7 | range_it = range(loops) 8 | 9 | t0 = pyperf.perf_counter() 10 | 11 | for loops in range_it: 12 | _ = pyvips.Operation.call('black', 10, 10) 13 | 14 | return pyperf.perf_counter() - t0 15 | 16 | 17 | runner = pyperf.Runner() 18 | runner.bench_time_func('Operation.call', operation_call) 19 | -------------------------------------------------------------------------------- /examples/try4.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import sys 4 | import pyvips 5 | 6 | # import logging 7 | # logging.basicConfig(level = logging.DEBUG) 8 | 9 | a = pyvips.Image.new_from_file(sys.argv[1]) 10 | 11 | b = pyvips.Image.new_from_file(sys.argv[2]) 12 | 13 | c = a.join(b, pyvips.Direction.HORIZONTAL, 14 | expand=True, 15 | shim=100, 16 | align=pyvips.Align.CENTRE, 17 | background=[128, 255, 128]) 18 | 19 | c.write_to_file(sys.argv[3]) 20 | -------------------------------------------------------------------------------- /examples/try12.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import sys 4 | 5 | import pyvips 6 | 7 | im = pyvips.Image.new_from_file(sys.argv[1], access="sequential") 8 | 9 | footer = pyvips.Image.black(im.width, 150) 10 | left_text = pyvips.Image.text("left corner", dpi=300) 11 | right_text = pyvips.Image.text("right corner", dpi=300) 12 | footer = footer.insert(left_text, 50, 50) 13 | footer = footer.insert(right_text, im.width - right_text.width - 50, 50) 14 | footer = footer.ifthenelse(0, [255, 0, 0], blend=True) 15 | 16 | im = im.insert(footer, 0, im.height, expand=True) 17 | 18 | im.write_to_file(sys.argv[2]) 19 | -------------------------------------------------------------------------------- /examples/split-animation.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import sys 4 | import pyvips 5 | 6 | if len(sys.argv) != 3: 7 | print(f"usage: {sys.argv[0]} output-directory animated-image") 8 | sys.exit(1) 9 | 10 | # load the animation, chop into pages 11 | image = pyvips.Image.new_from_file(sys.argv[2], n=-1, access="sequential") 12 | 13 | delay = image.get("delay") 14 | print(f"delay array = {delay}") 15 | 16 | pages = image.pagesplit() 17 | print(f"writing frames to {sys.argv[1]}/ ...") 18 | for page_number in range(len(pages)): 19 | filename = f"{sys.argv[1]}/frame-{page_number:04}.tif" 20 | pages[page_number].write_to_file(filename) 21 | -------------------------------------------------------------------------------- /examples/watermark_image.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import sys 4 | import pyvips 5 | 6 | image = pyvips.Image.new_from_file(sys.argv[1], access="sequential") 7 | watermark = pyvips.Image.new_from_file(sys.argv[3], access="sequential") 8 | 9 | # downsize the image by 50% 10 | image = image.resize(0.5) 11 | 12 | # set the watermark alpha to 20% (multiply A of RGBA by 0.2). 13 | watermark *= [1, 1, 1, 0.2] 14 | 15 | # overlay the watermark at the bottom left, with a 100 pixel margin 16 | image = image.composite(watermark, "over", 17 | x=100, y=image.height - watermark.height - 100) 18 | 19 | image.write_to_file(sys.argv[2]) 20 | -------------------------------------------------------------------------------- /examples/try2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import logging 4 | import pyvips 5 | 6 | logging.basicConfig(level=logging.DEBUG) 7 | pyvips.cache_set_trace(True) 8 | 9 | try: 10 | a = pyvips.Image.new_from_file("/home/john/pics/babe.poop") 11 | except pyvips.Error as e: 12 | print(str(e)) 13 | 14 | a = pyvips.Image.new_from_file("/home/john/pics/babe.jpg") 15 | b = pyvips.Image.new_from_file("/home/john/pics/k2.jpg") 16 | 17 | print('a =', a) 18 | print('b =', b) 19 | 20 | out = pyvips.call("add", a, b) 21 | 22 | print('out =', out) 23 | 24 | out = a.add(b) 25 | 26 | print('out =', out) 27 | 28 | out = out.linear([1, 1, 1], [2, 2, 2]) 29 | 30 | out.write_to_file("x.v") 31 | -------------------------------------------------------------------------------- /examples/join-animation.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import sys 4 | import pyvips 5 | 6 | if len(sys.argv) < 3: 7 | print(f"usage: {sys.argv[0]} output-image frame1 frame2 ...") 8 | sys.exit(1) 9 | 10 | # the input images 11 | # assume these are all the same size 12 | images = [pyvips.Image.new_from_file(filename, access="sequential") 13 | for filename in sys.argv[2:]] 14 | 15 | animation = images[0].pagejoin(images[1:]) 16 | 17 | # frame delays are in milliseconds ... 300 is pretty slow! 18 | delay_array = [300] * len(images) 19 | animation.set_type(pyvips.GValue.array_int_type, "delay", delay_array) 20 | 21 | print(f"writing {sys.argv[1]} ...") 22 | animation.write_to_file(sys.argv[1]) 23 | -------------------------------------------------------------------------------- /doc/_static/collapse.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | // Default to collapsed 3 | $('dl.staticmethod, dl.method').addClass('collapsed') 4 | 5 | $('dl.staticmethod > dd > p:first-child, dl.method > dd > p:first-child').click(function(e) { 6 | $(this).parents().eq(1).toggleClass('collapsed'); 7 | }); 8 | 9 | // Attaching the hashchange event listener 10 | $(window).on('hashchange', function() { 11 | base = window.location.hash.replace(/\./g, '\\.'); 12 | base = $(base); 13 | base.parent().removeClass('collapsed'); 14 | }); 15 | 16 | // Manually triggering it if we have hash part in URL 17 | if (window.location.hash) { 18 | $(window).trigger('hashchange') 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /examples/stream.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | import requests 5 | import pyvips 6 | 7 | URL = "https://cdn.filestackcontent.com/bnTGtQw5ShqMPpxH2tMw" 8 | URLS = [URL] * int(sys.argv[1]) 9 | 10 | session = requests.Session() 11 | 12 | image = pyvips.Image.black(1500, 1500) 13 | 14 | for i, url in enumerate(URLS): 15 | print(f"loading {url} ...") 16 | stream = session.get(url, stream=True).raw 17 | 18 | source = pyvips.SourceCustom() 19 | source.on_read((lambda stream: stream.read)(stream)) 20 | 21 | tile = pyvips.Image.new_from_source(source, "", access="sequential") 22 | image = image.composite2(tile, "over", x=50 * (i + 1), y=50 * (i + 1)) 23 | 24 | print("writing output.jpg ...") 25 | image.write_to_file("output.jpg") 26 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from os import path 4 | from setuptools import setup 5 | 6 | base_dir = path.dirname(__file__) 7 | src_dir = path.join(base_dir, 'pyvips') 8 | 9 | # When executing the setup.py, we need to be able to import ourselves, this 10 | # means that we need to add the pyvips/ directory to the sys.path. 11 | sys.path.insert(0, src_dir) 12 | 13 | # Try to install in API mode first, then if that fails, fall back to ABI 14 | 15 | # API mode requires a working C compiler plus all the libvips headers whereas 16 | # ABI only needs the libvips shared library to be on the system 17 | 18 | try: 19 | setup(cffi_modules=['pyvips/pyvips_build.py:ffibuilder']) 20 | except Exception as e: 21 | print(f'Falling back to ABI mode. Details: {e}') 22 | setup() 23 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | .. include global.rst 2 | 3 | `pyvips` -- Image processing with libvips 4 | ========================================= 5 | 6 | .. module:: pyvips 7 | :synopsis: Interface to the libvips image processing library. 8 | .. moduleauthor:: John Cupitt 9 | .. moduleauthor:: Kleis Auke Wolthuizen 10 | 11 | Contents 12 | -------- 13 | 14 | .. toctree:: 15 | :maxdepth: 2 16 | 17 | README 18 | intro 19 | vimage 20 | base 21 | error 22 | enums 23 | vconnection 24 | vsource 25 | vtarget 26 | vsourcecustom 27 | vtargetcustom 28 | vinterpolate 29 | gvalue 30 | gobject 31 | voperation 32 | vobject 33 | 34 | Indicies 35 | -------- 36 | 37 | * :ref:`genindex` 38 | * :ref:`modindex` 39 | * :ref:`search` 40 | -------------------------------------------------------------------------------- /examples/convolve.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ example pyvips code to convolve an image with a 3x3 mask 3 | 4 | args: inputfile outputfile 5 | no error checking for simplicity 6 | Notes: the convolution mask can be other sizes, 7 | here we use a scale of 1 (no scaling - see libvips conv docs) 8 | https://www.libvips.org/API/current/libvips-convolution.html#vips-conv 9 | and offset of 128 so zero output is mid-grey 10 | """ 11 | import pyvips 12 | import sys 13 | 14 | image = pyvips.Image.new_from_file(sys.argv[1], access='sequential') 15 | mask = pyvips.Image.new_from_array([[1, 1, 1], 16 | [1, -8, 1], 17 | [1, 1, 1] 18 | ], scale=1, offset=128) 19 | image = image.conv(mask, precision='integer') 20 | image.write_to_file(sys.argv[2]) 21 | -------------------------------------------------------------------------------- /examples/annotate-animation.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import sys 4 | import pyvips 5 | 6 | if len(sys.argv) != 4: 7 | print(f"usage: {sys.argv[0]} input-animation output-animation text") 8 | sys.exit(1) 9 | 10 | text = pyvips.Image.text(sys.argv[3], dpi=300, rgba=True) 11 | 12 | 13 | # draw an overlay on a page ... this could do anything 14 | def process_page(page, i): 15 | return page.composite(text, "over", 16 | x=(i * 4) % page.width, 17 | y=(i * 4) % page.height) 18 | 19 | 20 | # load the animation, chop into pages, rejoin, save 21 | animation = pyvips.Image.new_from_file(sys.argv[1], n=-1, access="sequential") 22 | pages = animation.pagesplit() 23 | pages = [process_page(page, i) for page, i in zip(pages, range(len(pages)))] 24 | animation = pages[0].pagejoin(pages[1:]) 25 | animation.write_to_file(sys.argv[2]) 26 | -------------------------------------------------------------------------------- /tests/perf/vips-bench.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pyperf 4 | import pyvips 5 | 6 | 7 | def vips_bench(loops): 8 | range_it = range(loops) 9 | 10 | t0 = pyperf.perf_counter() 11 | 12 | for loops in range_it: 13 | im = pyvips.Image.new_from_file("tmp/x.tif", access='sequential') 14 | 15 | im = im.crop(100, 100, im.width - 200, im.height - 200) 16 | im = im.reduce(1.0 / 0.9, 1.0 / 0.9, kernel='linear') 17 | mask = pyvips.Image.new_from_array([[-1, -1, -1], 18 | [-1, 16, -1], 19 | [-1, -1, -1]], scale=8) 20 | im = im.conv(mask, precision='integer') 21 | 22 | im.write_to_file("tmp/x2.tif") 23 | 24 | return pyperf.perf_counter() - t0 25 | 26 | 27 | runner = pyperf.Runner() 28 | runner.bench_time_func('vips bench', vips_bench) 29 | -------------------------------------------------------------------------------- /tests/perf/README.rst: -------------------------------------------------------------------------------- 1 | pyvips Benchmarks 2 | ================= 3 | 4 | Prepare 5 | ------- 6 | 7 | Prepare test-images:: 8 | 9 | $ mkdir tmp/ 10 | $ vips colourspace images/sample2.v tmp/t1.v srgb 11 | $ vips replicate tmp/t1.v tmp/t2.v 20 15 12 | $ vips extract_area tmp/t2.v tmp/x.tif[tile] 0 0 5000 5000 13 | $ vips copy tmp/x.tif tmp/x.jpg 14 | $ vipsheader tmp/x.tif 15 | 16 | 17 | Run 18 | --- 19 | 20 | :: 21 | 22 | # tune your system to run stable benchmarks 23 | $ python3 -m pyperf system tune 24 | 25 | $ python3 vips-bench.py -o vips-bench.json 26 | $ python3 -m pyperf stats vips-bench.json 27 | 28 | $ python3 operation-call.py -o operation-call.json 29 | $ python3 -m pyperf stats operation-call.json 30 | 31 | # command to test if a difference is significant 32 | $ python3 -m pyperf compare_to operation-call2.json operation-call.json --table 33 | -------------------------------------------------------------------------------- /examples/thumb-dir.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | """ 3 | example pyvips code to run thumbnail on a dir full of images 4 | 5 | https://libvips.github.io/pyvips/vimage.html?highlight=thumbnail#pyvips.Image.thumbnail 6 | """ 7 | 8 | import os 9 | import multiprocessing 10 | import sys 11 | import pyvips 12 | 13 | 14 | def thumbnail(directory, filename): 15 | try: 16 | name, extension = os.path.splitext(filename) 17 | thumb = pyvips.Image.thumbnail(f"{directory}/{filename}", 128) 18 | thumb.write_to_file(f"{directory}/tn_{name}.jpg") 19 | except pyvips.Error: 20 | # ignore failures due to eg. not an image 21 | pass 22 | 23 | 24 | def all_files(path): 25 | for (root, dirs, files) in os.walk(path): 26 | for file in files: 27 | yield root, file 28 | 29 | 30 | with multiprocessing.Pool() as pool: 31 | pool.starmap(thumbnail, all_files(sys.argv[1])) 32 | -------------------------------------------------------------------------------- /tests/perf/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Prepare test-images 4 | 5 | if [ ! -d tmp ]; then 6 | echo building test image ... 7 | mkdir tmp/ 8 | vips colourspace images/sample2.v tmp/t1.v srgb 9 | vips replicate tmp/t1.v tmp/t2.v 20 15 10 | vips extract_area tmp/t2.v tmp/x.tif[tile] 0 0 5000 5000 11 | vips copy tmp/x.tif tmp/x.jpg 12 | vipsheader tmp/x.tif 13 | fi 14 | 15 | # tune your system to run stable benchmarks ... this needs to run as root 16 | # since it changes some kernel settings 17 | echo please run: 18 | echo " sudo python3 -m pyperf system tune" 19 | echo to put your system into a stable benchmarking state 20 | 21 | # run tests 22 | echo testing vips-bench.py ... 23 | python3 vips-bench.py -o vips-bench.json 24 | python3 -m pyperf stats vips-bench.json 25 | 26 | echo testing operation-call.py ... 27 | python3 operation-call.py -o operation-call.json 28 | python3 -m pyperf stats operation-call.json 29 | 30 | # command to test if a difference is significant 31 | # python3 -m pyperf compare_to operation-call2.json operation-call.json --table 32 | 33 | -------------------------------------------------------------------------------- /examples/watermark.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import sys 4 | import pyvips 5 | 6 | im = pyvips.Image.new_from_file(sys.argv[1], access="sequential") 7 | 8 | text = pyvips.Image.text(f"{sys.argv[3]}", 9 | width=500, 10 | dpi=100, 11 | align="centre", 12 | rgba=True) 13 | 14 | # scale the alpha down to make the text semi-transparent 15 | text = (text * [1, 1, 1, 0.3]).cast("uchar") 16 | 17 | text = text.rotate(45) 18 | 19 | # tile to the size of the image page, then tile again to the full image size 20 | text = text.embed(10, 10, text.width + 20, text.width + 20) 21 | page_height = im.get_page_height() 22 | text = text.replicate(1 + im.width / text.width, 1 + page_height / text.height) 23 | text = text.crop(0, 0, im.width, page_height) 24 | text = text.replicate(1, 1 + im.height / text.height) 25 | text = text.crop(0, 0, im.width, im.height) 26 | 27 | # composite the two layers 28 | im = im.composite(text, "over") 29 | 30 | im.write_to_file(sys.argv[2]) 31 | -------------------------------------------------------------------------------- /examples/progress.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import pyvips 4 | 5 | 6 | def progress_print(name, progress): 7 | print(f'signal {name}:') 8 | print(f' run = {progress.run} (seconds of run time)') 9 | print(f' eta = {progress.eta} (estimated seconds left)') 10 | print(f' tpels = {progress.tpels} (total number of pels)') 11 | print(f' npels = {progress.npels} (number of pels computed so far)') 12 | print(f' percent = {progress.percent} (percent complete)') 13 | 14 | 15 | def preeval_cb(image, progress): 16 | progress_print('preeval', progress) 17 | 18 | 19 | def eval_cb(image, progress): 20 | progress_print('eval', progress) 21 | 22 | # you can kill computation if necessary 23 | if progress.percent > 50: 24 | image.set_kill(True) 25 | 26 | 27 | def posteval_cb(image, progress): 28 | progress_print('posteval', progress) 29 | 30 | 31 | image = pyvips.Image.black(1, 500) 32 | image.set_progress(True) 33 | image.signal_connect('preeval', preeval_cb) 34 | image.signal_connect('eval', eval_cb) 35 | image.signal_connect('posteval', posteval_cb) 36 | image.avg() 37 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 John Cupitt 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /examples/pil-numpy-pyvips.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import sys 4 | import time 5 | 6 | import pyvips 7 | from PIL import Image 8 | import numpy as np 9 | 10 | if len(sys.argv) != 3: 11 | print(f'usage: {sys.argv[0]} input-filename output-filename') 12 | sys.exit(-1) 13 | 14 | 15 | # load with PIL 16 | start_pillow = time.time() 17 | pillow_img = np.asarray(Image.open(sys.argv[1])) 18 | print('Pillow Time:', time.time() - start_pillow) 19 | print('pil shape', pillow_img.shape) 20 | 21 | # load with vips to a memory array 22 | start_vips = time.time() 23 | img = pyvips.Image.new_from_file(sys.argv[1]) 24 | np_3d = np.asarray(img) # or img.numpy() 25 | 26 | print('Vips Time:', time.time() - start_vips) 27 | print('vips shape', np_3d.shape) 28 | 29 | # make a vips image from the PIL image 30 | vi = pyvips.Image.new_from_array(pillow_img) 31 | 32 | # verify we have the same result 33 | # this can be non-zero for formats like jpg if the two libraries are using 34 | # different libjpg versions ... try with png instead 35 | print('Average pil/vips difference:', (vi - img).avg()) 36 | 37 | # and write back to disc for checking 38 | vi.write_to_file(sys.argv[2]) 39 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | CI: 7 | name: "Linux x64 (Ubuntu 22.04) - Python ${{ matrix.python-version }}" 8 | runs-on: ubuntu-22.04 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | # Only test supported Python versions: 13 | # https://endoflife.date/python 14 | python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "pypy3.9"] 15 | 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@v4 19 | 20 | - name: Set up Python ${{ matrix.python-version }} 21 | uses: actions/setup-python@v5 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | 25 | - name: Install libvips 26 | run: | 27 | sudo apt-get update 28 | sudo apt-get install --no-install-recommends libvips 29 | 30 | - name: Lint with flake8 31 | run: | 32 | pip install flake8 33 | flake8 . 34 | 35 | - name: Install tox and any other packages 36 | run: pip install tox 37 | 38 | - name: Run tox 39 | # Run tox using the version of Python in `PATH` 40 | run: tox -e py 41 | -------------------------------------------------------------------------------- /pyvips/vinterpolate.py: -------------------------------------------------------------------------------- 1 | import pyvips 2 | from pyvips import ffi, vips_lib, Error, _to_bytes 3 | 4 | # import logging 5 | # logger = logging.getLogger(__name__) 6 | 7 | 8 | class Interpolate(pyvips.VipsObject): 9 | """Make interpolators for operators like :meth:`.affine`. 10 | 11 | """ 12 | 13 | def __init__(self, pointer): 14 | # logger.debug('Operation.__init__: pointer = %s', pointer) 15 | super(Interpolate, self).__init__(pointer) 16 | 17 | @staticmethod 18 | def new(name): 19 | """Make a new interpolator by name. 20 | 21 | Make a new interpolator from the libvips class nickname. For example:: 22 | 23 | inter = pyvips.Interpolator.new('bicubic') 24 | 25 | You can get a list of all supported interpolators from the command-line 26 | with:: 27 | 28 | $ vips -l interpolate 29 | 30 | See for example :meth:`.affine`. 31 | 32 | """ 33 | 34 | # logger.debug('VipsInterpolate.new: name = %s', name) 35 | 36 | vi = vips_lib.vips_interpolate_new(_to_bytes(name)) 37 | if vi == ffi.NULL: 38 | raise Error(f'no such interpolator {name}') 39 | 40 | return Interpolate(vi) 41 | 42 | 43 | __all__ = ['Interpolate'] 44 | -------------------------------------------------------------------------------- /pyvips/vconnection.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import pyvips 4 | from pyvips import ffi, vips_lib, _to_string 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | 9 | class Connection(pyvips.VipsObject): 10 | """The abstract base Connection class. 11 | 12 | """ 13 | 14 | def __init__(self, pointer): 15 | # logger.debug('Operation.__init__: pointer = %s', pointer) 16 | super(Connection, self).__init__(pointer) 17 | 18 | def filename(self): 19 | """Get the filename associated with a connection. Return None if there 20 | is no associated file. 21 | 22 | """ 23 | 24 | so = ffi.cast('VipsConnection *', self.pointer) 25 | pointer = vips_lib.vips_connection_filename(so) 26 | if pointer == ffi.NULL: 27 | return None 28 | else: 29 | return _to_string(pointer) 30 | 31 | def nick(self): 32 | """Make a human-readable name for a connection suitable for error 33 | messages. 34 | 35 | """ 36 | 37 | so = ffi.cast('VipsConnection *', self.pointer) 38 | pointer = vips_lib.vips_connection_nick(so) 39 | if pointer == ffi.NULL: 40 | return None 41 | else: 42 | return _to_string(pointer) 43 | 44 | 45 | __all__ = ['Connection'] 46 | -------------------------------------------------------------------------------- /doc/_static/style.css: -------------------------------------------------------------------------------- 1 | dl.staticmethod.collapsed > dd > p:not(:first-child), 2 | dl.staticmethod.collapsed > dd > div, 3 | dl.staticmethod.collapsed > dd > dl, 4 | dl.staticmethod.collapsed > dd > table, 5 | dl.method.collapsed > dd > p:not(:first-child), 6 | dl.method.collapsed > dd > div, 7 | dl.method.collapsed > dd > dl, 8 | dl.method.collapsed > dd > table { 9 | display: none; 10 | } 11 | 12 | dl.staticmethod > dd > p:not(:first-child), 13 | dl.staticmethod > dd > div, 14 | dl.staticmethod > dd > dl, 15 | dl.staticmethod > dd > table, 16 | dl.method > dd > p:not(:first-child), 17 | dl.method > dd > div, 18 | dl.method > dd > dl, 19 | dl.method > dd > table { 20 | display: block; 21 | } 22 | 23 | dl.staticmethod > dd > p:not(:only-child):first-child:before, 24 | dl.method > dd > p:not(:only-child):first-child:before { 25 | position: absolute; 26 | font-family: "FontAwesome"; 27 | content: ''; 28 | display: inline-block; 29 | font-style: normal; 30 | font-weight: normal; 31 | line-height: 1; 32 | text-decoration: inherit; 33 | margin-top: 5px; 34 | margin-left: -20px; 35 | white-space: pre; 36 | cursor: pointer; 37 | } 38 | 39 | dl.staticmethod.collapsed > dd > p:not(:only-child):first-child:before, 40 | dl.method.collapsed > dd > p:not(:only-child):first-child:before { 41 | content: ''; 42 | } 43 | -------------------------------------------------------------------------------- /pyvips/pyvips_build.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | 3 | import pkgconfig 4 | from cffi import FFI 5 | 6 | # we must have the vips package to be able to do anything 7 | if not pkgconfig.exists('vips'): 8 | raise Exception('unable to find pkg-config package "vips"') 9 | if pkgconfig.installed('vips', '< 8.2'): 10 | raise Exception('pkg-config "vips" is too old -- need libvips 8.2 or later') 11 | 12 | major, minor, micro = [int(s) for s in pkgconfig.modversion('vips').split('.')] 13 | 14 | ffibuilder = FFI() 15 | 16 | # vips_value_set_blob_free and vips_area_free_cb compat for libvips < 8.6 17 | compat = ''' 18 | int 19 | vips_area_free_cb(void *mem, VipsArea *area) 20 | { 21 | g_free(mem); 22 | 23 | return 0; 24 | } 25 | 26 | void 27 | vips_value_set_blob_free(GValue* value, void* data, size_t length) 28 | { 29 | vips_value_set_blob(value, (VipsCallbackFn) vips_area_free_cb, 30 | data, length); 31 | } 32 | ''' if major == 8 and minor < 6 else '' 33 | 34 | ffibuilder.set_source("_libvips", 35 | f""" 36 | #include 37 | {compat} 38 | """, 39 | **pkgconfig.parse('vips')) 40 | 41 | features = { 42 | 'major': major, 43 | 'minor': minor, 44 | 'micro': micro, 45 | 'api': True, 46 | } 47 | 48 | 49 | import vdecls 50 | 51 | # handy for debugging 52 | # with open('vips-source.txt','w') as f: 53 | # c = vdecls.cdefs(features) 54 | # f.write(c) 55 | 56 | ffibuilder.cdef(vdecls.cdefs(features)) 57 | 58 | if __name__ == "__main__": 59 | ffibuilder.compile(verbose=True) 60 | -------------------------------------------------------------------------------- /tests/test_block.py: -------------------------------------------------------------------------------- 1 | # vim: set fileencoding=utf-8 : 2 | 3 | import tempfile 4 | 5 | import pytest 6 | import pyvips 7 | from helpers import skip_if_no, JPEG_FILE, WEBP_FILE, SVG_FILE 8 | 9 | 10 | class TestBlock: 11 | @classmethod 12 | def setup_class(cls): 13 | cls.tempdir = tempfile.mkdtemp() 14 | 15 | @skip_if_no('jpegload') 16 | @skip_if_no('webpload') 17 | @pytest.mark.skipif(not pyvips.at_least_libvips(8, 13), 18 | reason="requires libvips >= 8.13") 19 | def test_operation_block(self): 20 | # block all loads except jpeg 21 | pyvips.operation_block_set("VipsForeignLoad", True) 22 | pyvips.operation_block_set("VipsForeignLoadJpeg", False) 23 | 24 | image = pyvips.Image.new_from_file(JPEG_FILE) 25 | assert image.width == 1024 26 | 27 | # should fail 28 | with pytest.raises(Exception): 29 | _ = pyvips.Image.new_from_file(WEBP_FILE) 30 | 31 | # reenable all loads 32 | pyvips.operation_block_set("VipsForeignLoad", False) 33 | 34 | @skip_if_no('jpegload') 35 | @skip_if_no('svgload') 36 | @pytest.mark.skipif(not pyvips.at_least_libvips(8, 13), 37 | reason="requires libvips >= 8.13") 38 | def test_block_untrusted(self): 39 | # block all untrusted operations 40 | pyvips.block_untrusted_set(True) 41 | 42 | # should fail 43 | with pytest.raises(Exception): 44 | _ = pyvips.Image.new_from_file(SVG_FILE) 45 | 46 | # reenable all loads 47 | pyvips.block_untrusted_set(False) 48 | -------------------------------------------------------------------------------- /examples/shepard.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # Shepard's distortion, from https://github.com/tourtiere/light-distortion 4 | # with kind permission. 5 | 6 | # Use a set of control points to distort an image. 7 | # Usage: 8 | # ./shepard.py ~/pics/k2.jpg x.jpg "300,400 300,500 300,1500 300,1100" 9 | 10 | 11 | import re 12 | import sys 13 | from typing import Tuple, List 14 | 15 | import pyvips 16 | 17 | Point = Tuple[int, int] 18 | Couple = Tuple[Point, Point] 19 | 20 | 21 | def shepards(image: pyvips.Image, couples: List[Couple]) -> pyvips.Image: 22 | """Shepard's distortion. 23 | 24 | Distort an image with a set of control points. 25 | """ 26 | 27 | index = pyvips.Image.xyz(image.width, image.height) 28 | deltas = [] 29 | weights = [] 30 | for p1, p2 in couples: 31 | diff = index - list(p2) 32 | 33 | distance = (diff[0]**2 + diff[1]**2) 34 | distance = distance.ifthenelse(distance, 0.1) 35 | 36 | weight = 1.0 / distance 37 | 38 | delta = [(p1[0] - p2[0]), (p1[1] - p2[1])] * weight 39 | 40 | weights.append(weight) 41 | deltas.append(delta) 42 | 43 | # add, normalize 44 | index += pyvips.Image.sum(deltas) / pyvips.Image.sum(weights) 45 | 46 | return image.mapim(index, interpolate=pyvips.Interpolate.new('bicubic')) 47 | 48 | 49 | if __name__ == '__main__': 50 | image = pyvips.Image.new_from_file(sys.argv[1]) 51 | matches = re.findall(r'(\d+),(\d+) (\d+),(\d+)', sys.argv[3]) 52 | couples = [((int(m[0]), int(m[1])), (int(m[2]), int(m[3]))) 53 | for m in matches] 54 | 55 | image = shepards(image, couples) 56 | 57 | image.write_to_file(sys.argv[2]) 58 | -------------------------------------------------------------------------------- /examples/cod.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import sys 4 | import pyvips 5 | 6 | # import logging 7 | # logging.basicConfig(level = logging.DEBUG) 8 | # pyvips.cache_set_trace(True) 9 | 10 | 11 | def to_polar(image): 12 | """Transform image coordinates to polar. 13 | 14 | The image is transformed so that it is wrapped around a point in the 15 | centre. Vertical straight lines become circles or segments of circles, 16 | horizontal straight lines become radial spokes. 17 | """ 18 | # xy image, origin in the centre, scaled to fit image to a circle 19 | xy = pyvips.Image.xyz(image.width, image.height) 20 | xy -= [image.width / 2.0, image.height / 2.0] 21 | scale = min(image.width, image.height) / float(image.width) 22 | xy *= 2.0 / scale 23 | 24 | index = xy.polar() 25 | 26 | # scale vertical axis to 360 degrees 27 | index *= [1, image.height / 360.0] 28 | 29 | return image.mapim(index) 30 | 31 | 32 | def to_rectangular(image): 33 | """Transform image coordinates to rectangular. 34 | 35 | The image is transformed so that it is unwrapped from a point in the 36 | centre. Circles or segments of circles become vertical straight lines, 37 | radial lines become horizontal lines. 38 | """ 39 | # xy image, vertical scaled to 360 degrees 40 | xy = pyvips.Image.xyz(image.width, image.height) 41 | xy *= [1, 360.0 / image.height] 42 | 43 | index = xy.rect() 44 | 45 | # scale to image rect 46 | scale = min(image.width, image.height) / float(image.width) 47 | index *= scale / 2.0 48 | index += [image.width / 2.0, image.height / 2.0] 49 | 50 | return image.mapim(index) 51 | 52 | 53 | a = pyvips.Image.new_from_file(sys.argv[1]) 54 | a = to_polar(a) 55 | a = to_rectangular(a) 56 | a.write_to_file(sys.argv[2]) 57 | -------------------------------------------------------------------------------- /examples/try5.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import sys 4 | import pyvips 5 | 6 | # import logging 7 | # logging.basicConfig(level = logging.DEBUG) 8 | 9 | a = pyvips.Image.new_from_file(sys.argv[1]) 10 | 11 | 12 | def should_equal(test, a, b): 13 | if abs(a - b) > 0.01: 14 | print(f'{test}: seen {a:g} and {b:g}') 15 | sys.exit(1) 16 | 17 | 18 | def bandsplit(a): 19 | return [a.extract_band(i) for i in range(0, a.bands)] 20 | 21 | 22 | # test operator overloads 23 | 24 | # addition 25 | b = a + 12 26 | should_equal('add constant', a.avg() + 12, b.avg()) 27 | 28 | b = a + [12, 0, 0] 29 | x = map(lambda x: x.avg()) 30 | bandsplit(a) 31 | y = map(lambda x: x.avg()) 32 | bandsplit(b) 33 | x[0] += 12 34 | should_equal('add multiband constant', sum(x), sum(y)) 35 | 36 | b = a + [12, 0, 0] 37 | b = a + b 38 | b = 12 + a 39 | b = [12, 0, 0] + a 40 | 41 | b = a - 12 42 | b = a - [12, 0, 0] 43 | b = a - b 44 | b = 12 - a 45 | b = [12, 0, 0] - a 46 | 47 | b = a * 12 48 | b = a * [12, 1, 1] 49 | b = a * b 50 | b = 12 * a 51 | b = [12, 1, 1] * a 52 | 53 | b = a / 12 54 | b = a / [12, 1, 1] 55 | b = 12 / a 56 | b = [12, 1, 1] / a 57 | b = a / b 58 | 59 | b = a // 12 60 | b = a // [12, 1, 1] 61 | b = 12 // a 62 | b = [12, 1, 1] // a 63 | b = a // b 64 | 65 | b = a % 12 66 | b = a % [12, 1, 1] 67 | b = a % b 68 | 69 | b = a ** 12 70 | b = a ** [12, 1, 1] 71 | b = 12 ** a 72 | b = [12, 1, 1] ** a 73 | b = a ** b 74 | 75 | b = a << 12 76 | b = a << [12, 1, 1] 77 | b = a << b 78 | 79 | b = a >> 12 80 | b = a >> [12, 1, 1] 81 | b = a >> b 82 | 83 | b = a & 12 84 | b = a & [12, 1, 1] 85 | b = 12 & a 86 | b = [12, 1, 1] & a 87 | b = a & b 88 | 89 | b = a | 12 90 | b = a | [12, 1, 1] 91 | b = 12 | a 92 | b = [12, 1, 1] | a 93 | b = a | b 94 | 95 | b = a ^ 12 96 | b = a ^ [12, 1, 1] 97 | b = 12 ^ a 98 | b = [12, 1, 1] ^ a 99 | b = a ^ b 100 | 101 | b = -a 102 | b = +a 103 | b = abs(a) 104 | b = ~a 105 | -------------------------------------------------------------------------------- /examples/connection.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import sys 4 | import pyvips 5 | 6 | if len(sys.argv) != 4: 7 | print(f"usage: {sys.argv[0]} IN-FILE OUT-FILE FORMAT") 8 | print(f" eg.: {sys.argv[0]} ~/pics/k2.jpg x .tif[tile]") 9 | sys.exit(1) 10 | 11 | 12 | def source_custom(filename): 13 | input_file = open(sys.argv[1], "rb") 14 | 15 | def read_handler(size): 16 | return input_file.read(size) 17 | 18 | # seek is optional, but may improve performance by reducing buffering 19 | def seek_handler(offset, whence): 20 | input_file.seek(offset, whence) 21 | return input_file.tell() 22 | 23 | source = pyvips.SourceCustom() 24 | source.on_read(read_handler) 25 | source.on_seek(seek_handler) 26 | 27 | return source 28 | 29 | 30 | def target_custom(filename): 31 | # w+ means read and write ... we need to be able to read from our output 32 | # stream for TIFF write 33 | output_file = open(sys.argv[2], "w+b") 34 | 35 | def write_handler(chunk): 36 | return output_file.write(chunk) 37 | 38 | # read and seek are optional and only needed for formats like TIFF 39 | def read_handler(size): 40 | return output_file.read(size) 41 | 42 | def seek_handler(offset, whence): 43 | output_file.seek(offset, whence) 44 | return output_file.tell() 45 | 46 | def end_handler(): 47 | # you can't throw exceptions over on_ handlers, you must return an 48 | # error code 49 | try: 50 | output_file.close() 51 | except IOError: 52 | return -1 53 | else: 54 | return 0 55 | 56 | target = pyvips.TargetCustom() 57 | target.on_write(write_handler) 58 | target.on_read(read_handler) 59 | target.on_seek(seek_handler) 60 | target.on_end(end_handler) 61 | 62 | return target 63 | 64 | 65 | source = source_custom(sys.argv[1]) 66 | target = target_custom(sys.argv[2]) 67 | image = pyvips.Image.new_from_source(source, '', access='sequential') 68 | image.write_to_target(target, sys.argv[3]) 69 | -------------------------------------------------------------------------------- /pyvips/vregion.py: -------------------------------------------------------------------------------- 1 | # wrap VipsRegion 2 | 3 | import pyvips 4 | from pyvips import ffi, glib_lib, vips_lib, Error, at_least_libvips 5 | 6 | 7 | class Region(pyvips.VipsObject): 8 | """Wrap a VipsRegion object. 9 | 10 | """ 11 | 12 | def __init__(self, pointer): 13 | # logger.debug('Image.__init__: pointer = %s', pointer) 14 | super(Region, self).__init__(pointer) 15 | 16 | # constructors 17 | 18 | @staticmethod 19 | def new(image): 20 | """Make a region on an image. 21 | 22 | Returns: 23 | A new :class:`.Region`. 24 | 25 | Raises: 26 | :class:`.Error` 27 | 28 | """ 29 | 30 | pointer = vips_lib.vips_region_new(image.pointer) 31 | if pointer == ffi.NULL: 32 | raise Error('unable to make region') 33 | 34 | return pyvips.Region(pointer) 35 | 36 | def width(self): 37 | """Width of pixels held by region.""" 38 | if not at_least_libvips(8, 8): 39 | raise Error('libvips too old') 40 | 41 | return vips_lib.vips_region_width(self.pointer) 42 | 43 | def height(self): 44 | """Height of pixels held by region.""" 45 | if not at_least_libvips(8, 8): 46 | raise Error('libvips too old') 47 | 48 | return vips_lib.vips_region_height(self.pointer) 49 | 50 | def fetch(self, x, y, w, h): 51 | """Fill a region with pixel data. 52 | 53 | Pixels are filled with data! 54 | 55 | Returns: 56 | Pixel data. 57 | 58 | Raises: 59 | :class:`.Error` 60 | 61 | """ 62 | 63 | if not at_least_libvips(8, 8): 64 | raise Error('libvips too old') 65 | 66 | psize = ffi.new('size_t *') 67 | pointer = vips_lib.vips_region_fetch(self.pointer, x, y, w, h, psize) 68 | if pointer == ffi.NULL: 69 | raise Error('unable to fetch from region') 70 | 71 | pointer = ffi.gc(pointer, glib_lib.g_free) 72 | return ffi.buffer(pointer, psize[0]) 73 | 74 | 75 | __all__ = ['Region'] 76 | -------------------------------------------------------------------------------- /pyvips/error.py: -------------------------------------------------------------------------------- 1 | # errors from libvips 2 | 3 | import logging 4 | 5 | from pathlib import Path 6 | from pyvips import ffi, vips_lib, glib_lib 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | def _to_bytes(x): 12 | """Convert to a byte string. 13 | 14 | Convert a Python unicode string or a pathlib.Path to a utf-8-encoded 15 | byte string. You must call this on strings you pass to libvips. 16 | 17 | """ 18 | if isinstance(x, (str, Path)): 19 | # n.b. str also converts pathlib.Path objects 20 | x = str(x).encode('utf-8') 21 | 22 | return x 23 | 24 | 25 | def _to_string(x): 26 | """Convert to a unicode string. 27 | 28 | If x is a byte string, assume it is utf-8 and decode to a Python unicode 29 | string. You must call this on text strings you get back from libvips. 30 | 31 | """ 32 | if x == ffi.NULL: 33 | x = 'NULL' 34 | else: 35 | x = ffi.string(x) 36 | if isinstance(x, bytes): 37 | x = x.decode('utf-8') 38 | 39 | return x 40 | 41 | 42 | def _to_string_copy(x): 43 | """Convert to a unicode string, and auto-free. 44 | 45 | As _to_string(), but also tag x as a pointer to a memory area that must 46 | be freed with g_free(). 47 | 48 | """ 49 | return _to_string(ffi.gc(x, glib_lib.g_free)) 50 | 51 | 52 | class Error(Exception): 53 | """An error from vips. 54 | 55 | Attributes: 56 | message (str): a high-level description of the error 57 | detail (str): a string with some detailed diagnostics 58 | 59 | """ 60 | 61 | def __init__(self, message, detail=None): 62 | self.message = message 63 | if detail is None or detail == "": 64 | detail = _to_string(vips_lib.vips_error_buffer()) 65 | vips_lib.vips_error_clear() 66 | self.detail = detail 67 | 68 | logger.debug('Error %s %s', self.message, self.detail) 69 | 70 | def __str__(self): 71 | return f'{self.message}\n {self.detail}' 72 | 73 | 74 | __all__ = [ 75 | '_to_bytes', 76 | '_to_string', 77 | '_to_string_copy', 78 | 'Error' 79 | ] 80 | -------------------------------------------------------------------------------- /pyvips/vsourcecustom.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import pyvips 4 | from pyvips import ffi, vips_lib 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | 9 | class SourceCustom(pyvips.Source): 10 | """Custom sources allow reading data from otherwise unsupported sources. 11 | Requires libvips `>= 8.9.0`. 12 | 13 | To use, create a SourceCustom object, then provide callbacks to 14 | :meth:`on_read` and :meth:`on_seek`. 15 | """ 16 | 17 | def __init__(self): 18 | """Make a new custom source. 19 | 20 | You can pass this source to (for example) :meth:`new_from_source`. 21 | 22 | """ 23 | 24 | source = ffi.cast('VipsSource*', vips_lib.vips_source_custom_new()) 25 | super(SourceCustom, self).__init__(source) 26 | 27 | def on_read(self, handler): 28 | """Attach a read handler. 29 | 30 | The interface is exactly as io.read(). The handler is given a number 31 | of bytes to fetch, and should return a bytes-like object containing up 32 | to that number of bytes. If there is no more data available, it should 33 | return None. 34 | 35 | """ 36 | 37 | def interface_handler(buf): 38 | chunk = handler(len(buf)) 39 | if chunk is None: 40 | return 0 41 | 42 | bytes_read = len(chunk) 43 | buf[:bytes_read] = chunk 44 | 45 | return bytes_read 46 | 47 | self.signal_connect("read", interface_handler) 48 | 49 | def on_seek(self, handler): 50 | """Attach a seek handler. 51 | 52 | The interface is the same as io.seek(), so the handler is passed 53 | parameters for offset and whence with the same meanings. 54 | 55 | However, the handler MUST return the new seek position. A simple way 56 | to do this is to call io.tell() and return that result. 57 | 58 | Seek handlers are optional. If you do not set one, your source will be 59 | treated as unseekable and libvips will do extra caching. 60 | 61 | """ 62 | 63 | self.signal_connect("seek", handler) 64 | 65 | 66 | __all__ = ['SourceCustom'] 67 | -------------------------------------------------------------------------------- /tests/test_saveload.py: -------------------------------------------------------------------------------- 1 | # vim: set fileencoding=utf-8 : 2 | 3 | import os 4 | import tempfile 5 | 6 | import pyvips 7 | from pathlib import Path 8 | from helpers import temp_filename, skip_if_no, IMAGES, JPEG_FILE 9 | 10 | 11 | class TestSaveLoad: 12 | @classmethod 13 | def setup_class(cls): 14 | cls.tempdir = tempfile.mkdtemp() 15 | 16 | @skip_if_no('jpegload') 17 | def test_save_file(self): 18 | filename = temp_filename(self.tempdir, '.jpg') 19 | 20 | im = pyvips.Image.black(10, 20) 21 | im.write_to_file(filename) 22 | assert os.path.isfile(filename) 23 | 24 | os.remove(filename) 25 | 26 | @skip_if_no('jpegload') 27 | def test_load_file(self): 28 | filename = temp_filename(self.tempdir, '.jpg') 29 | 30 | im = pyvips.Image.black(10, 20) 31 | im.write_to_file(filename) 32 | 33 | x = pyvips.Image.new_from_file(filename) 34 | assert x.width == 10 35 | assert x.height == 20 36 | assert x.bands == 1 37 | 38 | os.remove(x.filename) 39 | 40 | @skip_if_no('jpegload') 41 | def test_save_file_pathlib(self): 42 | filename = Path(temp_filename(self.tempdir, '.jpg')) 43 | 44 | im = pyvips.Image.black(10, 20) 45 | im.write_to_file(filename) 46 | assert filename.exists() 47 | filename.unlink() 48 | 49 | @skip_if_no('jpegload') 50 | def test_load_file_pathlib(self): 51 | filename = Path(IMAGES) / 'sample.jpg' 52 | assert filename.exists() 53 | 54 | im_a = pyvips.Image.new_from_file(JPEG_FILE) 55 | im_b = pyvips.Image.new_from_file(filename) 56 | 57 | assert im_a.bands == im_b.bands 58 | assert im_a.width == im_b.width 59 | assert im_a.height == im_b.height 60 | 61 | @skip_if_no('jpegload') 62 | def test_save_buffer(self): 63 | im = pyvips.Image.black(10, 20) 64 | buf = im.write_to_buffer('.jpg') 65 | assert len(buf) > 100 66 | 67 | @skip_if_no('jpegload') 68 | def test_load_buffer(self): 69 | im = pyvips.Image.black(10, 20) 70 | buf = im.write_to_buffer('.jpg') 71 | 72 | x = pyvips.Image.new_from_buffer(buf, '') 73 | assert x.width == 10 74 | assert x.height == 20 75 | assert x.bands == 1 76 | -------------------------------------------------------------------------------- /tests/test_progress.py: -------------------------------------------------------------------------------- 1 | # vim: set fileencoding=utf-8 : 2 | 3 | import pytest 4 | 5 | import pyvips 6 | 7 | 8 | class TestProgress: 9 | def test_progress(self): 10 | seen_preeval = False 11 | seen_eval = False 12 | seen_posteval = False 13 | 14 | def preeval_cb(image, progress): 15 | nonlocal seen_preeval 16 | seen_preeval = True 17 | 18 | def eval_cb(image, progress): 19 | nonlocal seen_eval 20 | seen_eval = True 21 | 22 | def posteval_cb(image, progress): 23 | nonlocal seen_posteval 24 | seen_posteval = True 25 | 26 | image = pyvips.Image.black(1, 100000) 27 | image.set_progress(True) 28 | image.signal_connect('preeval', preeval_cb) 29 | image.signal_connect('eval', eval_cb) 30 | image.signal_connect('posteval', posteval_cb) 31 | image.avg() 32 | 33 | assert seen_preeval 34 | assert seen_eval 35 | assert seen_posteval 36 | 37 | def test_progress_fields(self): 38 | def preeval_cb(image, progress): 39 | assert progress.run == 0 40 | assert progress.eta == 0 41 | assert progress.percent == 0 42 | assert progress.tpels == 10000 43 | assert progress.npels == 0 44 | 45 | def eval_cb(image, progress): 46 | pass 47 | 48 | def posteval_cb(image, progress): 49 | assert progress.percent == 100 50 | assert progress.tpels == 10000 51 | assert progress.npels == 10000 52 | 53 | image = pyvips.Image.black(10, 1000) 54 | image.set_progress(True) 55 | image.signal_connect('preeval', preeval_cb) 56 | image.signal_connect('eval', eval_cb) 57 | image.signal_connect('posteval', posteval_cb) 58 | image.avg() 59 | 60 | def test_progress_kill(self): 61 | def preeval_cb(image, progress): 62 | pass 63 | 64 | def eval_cb(image, progress): 65 | image.set_kill(True) 66 | 67 | def posteval_cb(image, progress): 68 | pass 69 | 70 | # has to be very tall to ensure the kill has enough threadpool loops 71 | # to work 72 | image = pyvips.Image.black(1, 1000000) 73 | image.set_progress(True) 74 | image.signal_connect('preeval', preeval_cb) 75 | image.signal_connect('eval', eval_cb) 76 | image.signal_connect('posteval', posteval_cb) 77 | 78 | with pytest.raises(Exception): 79 | image.copy_memory() 80 | -------------------------------------------------------------------------------- /pyvips/vtarget.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import pyvips 4 | from pyvips import ffi, vips_lib, Error, _to_bytes 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | 9 | class Target(pyvips.Connection): 10 | """An output connection. 11 | 12 | """ 13 | 14 | def __init__(self, pointer): 15 | # logger.debug('Operation.__init__: pointer = %s', pointer) 16 | super(Target, self).__init__(pointer) 17 | 18 | @staticmethod 19 | def new_to_descriptor(descriptor): 20 | """Make a new target to write to a file descriptor (a small 21 | integer). 22 | 23 | Make a new target that is attached to the descriptor. For example:: 24 | 25 | target = pyvips.Target.new_to_descriptor(1) 26 | 27 | Makes a descriptor attached to stdout. 28 | 29 | You can pass this target to (for example) :meth:`write_to_target`. 30 | 31 | """ 32 | 33 | # logger.debug('VipsTarget.new_to_descriptor: descriptor = %d', 34 | # descriptor) 35 | 36 | # targets are mutable, so we can't use the cache 37 | pointer = vips_lib.vips_target_new_to_descriptor(descriptor) 38 | if pointer == ffi.NULL: 39 | raise Error(f"can't create output target from descriptor " 40 | f'{descriptor}') 41 | 42 | return Target(pointer) 43 | 44 | @staticmethod 45 | def new_to_file(filename): 46 | """Make a new target to write to a file. 47 | 48 | Make a new target that will write to the named file. For example:: 49 | 50 | target = pyvips.Target.new_to_file("myfile.jpg") 51 | 52 | You can pass this target to (for example) :meth:`write_to_target`. 53 | 54 | """ 55 | 56 | # logger.debug('VipsTarget.new_to_file: filename = %s', filename) 57 | 58 | pointer = vips_lib.vips_target_new_to_file(_to_bytes(filename)) 59 | if pointer == ffi.NULL: 60 | raise Error(f"can't create output target from filename {filename}") 61 | 62 | return Target(pointer) 63 | 64 | @staticmethod 65 | def new_to_memory(): 66 | """Make a new target to write to an area of memory. 67 | 68 | Make a new target that will write to memory. For example:: 69 | 70 | target = pyvips.Target.new_to_memory() 71 | 72 | You can pass this target to (for example) :meth:`write_to_target`. 73 | 74 | After writing to the target, fetch the bytes from the target object 75 | with `target.get("blob")`. 76 | 77 | """ 78 | 79 | # logger.debug('VipsTarget.new_to_memory:') 80 | 81 | pointer = vips_lib.vips_target_new_to_memory() 82 | if pointer == ffi.NULL: 83 | raise Error("can't create output target from memory") 84 | 85 | return Target(pointer) 86 | 87 | 88 | __all__ = ['Target'] 89 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | # First version of setuptools to support pyproject.toml configuration 4 | "setuptools>=61.0.0", 5 | # Must be kept in sync with `project.dependencies` 6 | "cffi>=1.0.0", 7 | "pkgconfig>=1.5", 8 | ] 9 | build-backend = "setuptools.build_meta" 10 | 11 | [project] 12 | name = "pyvips" 13 | authors = [ 14 | {name = "John Cupitt", email = "jcupitt@gmail.com"}, 15 | ] 16 | description = "binding for the libvips image processing library" 17 | readme = "README.rst" 18 | keywords = [ 19 | "image processing", 20 | ] 21 | license = {text = "MIT"} 22 | requires-python = ">=3.7" 23 | classifiers = [ 24 | "Development Status :: 5 - Production/Stable", 25 | "Environment :: Console", 26 | "Intended Audience :: Developers", 27 | "Intended Audience :: Science/Research", 28 | "Topic :: Multimedia :: Graphics", 29 | "Topic :: Multimedia :: Graphics :: Graphics Conversion", 30 | "License :: OSI Approved :: MIT License", 31 | "Programming Language :: Python", 32 | "Programming Language :: Python :: 3", 33 | "Programming Language :: Python :: 3 :: Only", 34 | "Programming Language :: Python :: 3.7", 35 | "Programming Language :: Python :: 3.8", 36 | "Programming Language :: Python :: 3.9", 37 | "Programming Language :: Python :: 3.10", 38 | "Programming Language :: Python :: 3.11", 39 | "Programming Language :: Python :: 3.12", 40 | "Programming Language :: Python :: 3.13", 41 | "Programming Language :: Python :: 3.14", 42 | "Programming Language :: Python :: Implementation :: CPython", 43 | "Programming Language :: Python :: Implementation :: PyPy", 44 | ] 45 | dependencies = [ 46 | # Must be kept in sync with `build-system.requires` 47 | "cffi>=1.0.0", 48 | ] 49 | dynamic = [ 50 | "version", 51 | ] 52 | 53 | [project.urls] 54 | changelog = "https://github.com/libvips/pyvips/blob/master/CHANGELOG.rst" 55 | documentation = "https://libvips.github.io/pyvips/" 56 | funding = "https://opencollective.com/libvips" 57 | homepage = "https://github.com/libvips/pyvips" 58 | issues = "https://github.com/libvips/pyvips/issues" 59 | source = "https://github.com/libvips/pyvips" 60 | 61 | [tool.setuptools] 62 | # We try to compile as part of install, so we can't run in a ZIP 63 | zip-safe = false 64 | include-package-data = false 65 | 66 | [tool.setuptools.dynamic] 67 | version = {attr = "pyvips.version.__version__"} 68 | 69 | [tool.setuptools.packages.find] 70 | exclude = [ 71 | "doc*", 72 | "examples*", 73 | "tests*", 74 | ] 75 | 76 | [project.optional-dependencies] 77 | binary = ["pyvips-binary"] 78 | # All the following are used for our own testing 79 | tox = ["tox"] 80 | test = [ 81 | "pytest", 82 | "pyperf", 83 | ] 84 | sdist = ["build"] 85 | doc = [ 86 | "sphinx", 87 | "sphinx_rtd_theme", 88 | ] 89 | 90 | [tool.pytest.ini_options] 91 | norecursedirs = ["tests/helpers"] 92 | -------------------------------------------------------------------------------- /pyvips/vsource.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import pyvips 4 | from pyvips import ffi, vips_lib, Error, _to_bytes 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | 9 | class Source(pyvips.Connection): 10 | """An input connection. 11 | 12 | """ 13 | 14 | def __init__(self, pointer): 15 | # logger.debug('Operation.__init__: pointer = %s', pointer) 16 | super(Source, self).__init__(pointer) 17 | 18 | @staticmethod 19 | def new_from_descriptor(descriptor): 20 | """Make a new source from a file descriptor (a small integer). 21 | 22 | Make a new source that is attached to the descriptor. For example:: 23 | 24 | source = pyvips.Source.new_from_descriptor(0) 25 | 26 | Makes a descriptor attached to stdin. 27 | 28 | You can pass this source to (for example) :meth:`new_from_source`. 29 | 30 | """ 31 | 32 | # logger.debug('VipsSource.new_from_descriptor: descriptor = %d', 33 | # descriptor) 34 | 35 | # sources are mutable, so we can't use the cache 36 | pointer = vips_lib.vips_source_new_from_descriptor(descriptor) 37 | if pointer == ffi.NULL: 38 | raise Error(f"can't create source from descriptor {descriptor}") 39 | 40 | return Source(pointer) 41 | 42 | @staticmethod 43 | def new_from_file(filename): 44 | """Make a new source from a filename. 45 | 46 | Make a new source that is attached to the named file. For example:: 47 | 48 | source = pyvips.Source.new_from_file("myfile.jpg") 49 | 50 | You can pass this source to (for example) :meth:`new_from_source`. 51 | 52 | """ 53 | 54 | # logger.debug('VipsSource.new_from_file: filename = %s', 55 | # filename) 56 | 57 | pointer = vips_lib.vips_source_new_from_file(_to_bytes(filename)) 58 | if pointer == ffi.NULL: 59 | raise Error(f"can't create source from filename {filename}") 60 | 61 | return Source(pointer) 62 | 63 | @staticmethod 64 | def new_from_memory(data): 65 | """Make a new source from a memory object. 66 | 67 | Make a new source that is attached to the memory object. For example:: 68 | 69 | source = pyvips.Source.new_from_memory("myfile.jpg") 70 | 71 | You can pass this source to (for example) :meth:`new_from_source`. 72 | 73 | The memory object can be anything that supports the Python buffer or 74 | memoryview protocol. 75 | 76 | """ 77 | 78 | # logger.debug('VipsSource.new_from_memory:') 79 | 80 | start = ffi.from_buffer(data) 81 | nbytes = data.nbytes if hasattr(data, 'nbytes') else len(data) 82 | 83 | pointer = vips_lib.vips_source_new_from_memory(start, nbytes) 84 | if pointer == ffi.NULL: 85 | raise Error("can't create input source from memory") 86 | 87 | source = Source(pointer) 88 | 89 | # keep a secret reference to the input data to make sure it's not GCed 90 | source._references = [data] 91 | 92 | return source 93 | 94 | 95 | __all__ = ['Source'] 96 | -------------------------------------------------------------------------------- /examples/watermark_context.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # watermark an image, but do it in a context-sensitive way, so we will write 4 | # black if the area we are overlaying the text on is white, for example 5 | 6 | # usage: 7 | # ./watermark_context.py ~/pics/k2.jpg x.jpg "hello there!" 8 | 9 | import sys 10 | import pyvips 11 | 12 | 13 | # zero-excluding average of an image ... return as an array, with the average 14 | # for each band 15 | def avgze(image): 16 | # since we use histograms 17 | if image.format != "uchar" and image.format != "ushort": 18 | raise Exception("uchar and ushort images only") 19 | 20 | # take the histogram, and set the count for 0 pixels to 0, removing them 21 | histze = image.hist_find().insert(pyvips.Image.black(1, 1), 0, 0) 22 | 23 | # number of non-zero pixels in each band 24 | nnz = [histze[i].avg() * histze.width * histze.height 25 | for i in range(histze.bands)] 26 | 27 | # multiply by the identity function and we get the sum of non-zero 28 | # pixels ... for 16-bit images, we need a larger identity 29 | # function 30 | totalze = histze * pyvips.Image.identity(ushort=histze.width > 256) 31 | 32 | # find average value in each band 33 | avgze = [totalze[i].avg() * histze.width * histze.height / nnz[i] 34 | for i in range(totalze.bands)] 35 | 36 | return avgze 37 | 38 | 39 | # find an opposing colour ... we split the density range (0 .. mx) into three: 40 | # values in the bottom third move to the top, values in the top third move to 41 | # the bottom, and values in the middle also move to the bottom 42 | def oppose(value, mx): 43 | if value < mx / 3: 44 | # bottom goes up 45 | return mx / 3 - value + 2 * mx / 3 46 | elif value < 2 * mx / 3: 47 | # middle goes down 48 | return 2 * mx / 3 - value 49 | else: 50 | # top goes down 51 | return mx - value 52 | 53 | 54 | im = pyvips.Image.new_from_file(sys.argv[1]) 55 | 56 | text = pyvips.Image.text(sys.argv[3], width=500, dpi=300, align="centre") 57 | text = text.rotate(45) 58 | 59 | # the position of the overlay in the image 60 | left = 100 61 | top = im.height - text.height - 100 62 | 63 | # find the non-alpha image bands 64 | if im.hasalpha(): 65 | no_alpha = im.extract_band(0, n=im.bands - 1) 66 | else: 67 | no_alpha = im 68 | 69 | # the pixels we will render the overlay on top of 70 | bg = no_alpha.crop(left, top, text.width, text.height) 71 | 72 | # mask the background with the text, so all non-text areas become zero, and 73 | # find the zero-excluding average 74 | avg = avgze(text.ifthenelse(bg, 0)) 75 | 76 | # for each band, find the opposing value 77 | mx = 255 if im.format == "uchar" else 65535 78 | text_colour = [oppose(avg[i], mx) for i in range(len(avg))] 79 | 80 | # make an overlay ... we put solid colour into the image and set a faded 81 | # version of the text mask as the alpha 82 | overlay = bg.new_from_image(text_colour) 83 | overlay = overlay.bandjoin((text * 0.5).cast("uchar")) 84 | 85 | # and composite that on to the original image 86 | im = im.composite(overlay, "over", x=left, y=top) 87 | 88 | im.write_to_file(sys.argv[2]) 89 | -------------------------------------------------------------------------------- /pyvips/vtargetcustom.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import pyvips 4 | from pyvips import ffi, vips_lib, at_least_libvips 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | 9 | class TargetCustom(pyvips.Target): 10 | """An output target you can connect action signals to to implement 11 | behaviour. 12 | 13 | """ 14 | 15 | def __init__(self): 16 | """Make a new target you can customise. 17 | 18 | You can pass this target to (for example) :meth:`write_to_target`. 19 | 20 | """ 21 | 22 | target = ffi.cast('VipsTarget*', vips_lib.vips_target_custom_new()) 23 | super(TargetCustom, self).__init__(target) 24 | 25 | def on_write(self, handler): 26 | """Attach a write handler. 27 | 28 | The interface is exactly as io.write(). The handler is given a 29 | bytes-like object to write, and should return the number of bytes 30 | written. 31 | 32 | """ 33 | 34 | def interface_handler(buf): 35 | return handler(buf) 36 | 37 | self.signal_connect("write", interface_handler) 38 | 39 | def on_read(self, handler): 40 | """Attach a read handler. 41 | 42 | The interface is exactly as io.read(). The handler is given a number 43 | of bytes to fetch, and should return a bytes-like object containing up 44 | to that number of bytes. If there is no more data available, it should 45 | return None. 46 | 47 | Read handlers are optional for targets. If you do not set one, your 48 | target will be treated as unreadable and libvips will be unable to 49 | write some file types (just TIFF, as of the time of writing). 50 | 51 | """ 52 | 53 | def interface_handler(buf): 54 | chunk = handler(len(buf)) 55 | if chunk is None: 56 | return 0 57 | 58 | bytes_read = len(chunk) 59 | buf[:bytes_read] = chunk 60 | 61 | return bytes_read 62 | 63 | if at_least_libvips(8, 13): 64 | self.signal_connect("read", interface_handler) 65 | 66 | def on_seek(self, handler): 67 | """Attach a seek handler. 68 | 69 | The interface is the same as io.seek(), so the handler is passed 70 | parameters for offset and whence with the same meanings. 71 | 72 | However, the handler MUST return the new seek position. A simple way 73 | to do this is to call io.tell() and return that result. 74 | 75 | Seek handlers are optional. If you do not set one, your target will be 76 | treated as unseekable and libvips will be unable to write some file 77 | types (just TIFF, as of the time of writing). 78 | 79 | """ 80 | 81 | if at_least_libvips(8, 13): 82 | self.signal_connect("seek", handler) 83 | 84 | def on_end(self, handler): 85 | """Attach an end handler. 86 | 87 | This optional handler is called at the end of write. It should do any 88 | cleaning up necessary, and return 0 on success and -1 on error. 89 | 90 | """ 91 | 92 | if not at_least_libvips(8, 13): 93 | # fall back for older libvips 94 | self.on_finish(handler) 95 | else: 96 | self.signal_connect("end", handler) 97 | 98 | def on_finish(self, handler): 99 | """Attach a finish handler. 100 | 101 | For libvips 8.13 and later, this method is deprecated in favour of 102 | :meth:`on_end`. 103 | 104 | """ 105 | 106 | self.signal_connect("finish", handler) 107 | 108 | 109 | __all__ = ['TargetCustom'] 110 | -------------------------------------------------------------------------------- /tests/test_gvalue.py: -------------------------------------------------------------------------------- 1 | # vim: set fileencoding=utf-8 : 2 | import pytest 3 | 4 | import pyvips 5 | from helpers import JPEG_FILE, assert_almost_equal_objects 6 | 7 | 8 | class TestGValue: 9 | def test_bool(self): 10 | gv = pyvips.GValue() 11 | gv.set_type(pyvips.GValue.gbool_type) 12 | gv.set(True) 13 | value = gv.get() 14 | assert value 15 | 16 | gv.set(False) 17 | value = gv.get() 18 | assert not value 19 | 20 | def test_int(self): 21 | gv = pyvips.GValue() 22 | gv.set_type(pyvips.GValue.gint_type) 23 | gv.set(12) 24 | value = gv.get() 25 | assert value == 12 26 | 27 | def test_uint64(self): 28 | gv = pyvips.GValue() 29 | gv.set_type(pyvips.GValue.guint64_type) 30 | gv.set(2 ** 64 - 1) # G_MAXUINT64 31 | value = gv.get() 32 | assert value == 2 ** 64 - 1 33 | 34 | def test_double(self): 35 | gv = pyvips.GValue() 36 | gv.set_type(pyvips.GValue.gdouble_type) 37 | gv.set(3.1415) 38 | value = gv.get() 39 | assert value == 3.1415 40 | 41 | def test_enum(self): 42 | # the Interpretation enum is created when the first image is made -- 43 | # make it ourselves in case we are run before the first image 44 | pyvips.vips_lib.vips_interpretation_get_type() 45 | interpretation_gtype = pyvips.gobject_lib. \ 46 | g_type_from_name(b'VipsInterpretation') 47 | gv = pyvips.GValue() 48 | gv.set_type(interpretation_gtype) 49 | gv.set('xyz') 50 | value = gv.get() 51 | assert value == 'xyz' 52 | 53 | def test_flags(self): 54 | # the OperationFlags enum is created when the first op is made -- 55 | # make it ourselves in case we are run before that 56 | pyvips.vips_lib.vips_operation_flags_get_type() 57 | operationflags_gtype = pyvips.gobject_lib. \ 58 | g_type_from_name(b'VipsOperationFlags') 59 | gv = pyvips.GValue() 60 | gv.set_type(operationflags_gtype) 61 | gv.set(12) 62 | value = gv.get() 63 | assert value == 12 64 | 65 | # we also support setting flags with strings 66 | gv.set("deprecated") 67 | value = gv.get() 68 | assert value == 8 69 | 70 | # libvips 8.15 allows this as well 71 | # gv.set("deprecated|nocache") 72 | # though we don't test it 73 | 74 | def test_string(self): 75 | gv = pyvips.GValue() 76 | gv.set_type(pyvips.GValue.gstr_type) 77 | gv.set('banana') 78 | value = gv.get() 79 | assert value == 'banana' 80 | 81 | def test_array_int(self): 82 | gv = pyvips.GValue() 83 | gv.set_type(pyvips.GValue.array_int_type) 84 | gv.set([1, 2, 3]) 85 | value = gv.get() 86 | assert_almost_equal_objects(value, [1, 2, 3]) 87 | 88 | def test_array_double(self): 89 | gv = pyvips.GValue() 90 | gv.set_type(pyvips.GValue.array_double_type) 91 | gv.set([1.1, 2.1, 3.1]) 92 | value = gv.get() 93 | assert_almost_equal_objects(value, [1.1, 2.1, 3.1]) 94 | 95 | def test_image(self): 96 | image = pyvips.Image.new_from_file(JPEG_FILE) 97 | gv = pyvips.GValue() 98 | gv.set_type(pyvips.GValue.image_type) 99 | gv.set(image) 100 | value = gv.get() 101 | assert value == image 102 | 103 | def test_array_image(self): 104 | image = pyvips.Image.new_from_file(JPEG_FILE) 105 | r, g, b = image.bandsplit() 106 | gv = pyvips.GValue() 107 | gv.set_type(pyvips.GValue.array_image_type) 108 | gv.set([r, g, b]) 109 | value = gv.get() 110 | assert value, [r, g == b] 111 | 112 | def test_blob(self): 113 | with open(JPEG_FILE, 'rb') as f: 114 | blob = f.read() 115 | gv = pyvips.GValue() 116 | gv.set_type(pyvips.GValue.blob_type) 117 | gv.set(blob) 118 | value = gv.get() 119 | assert value == blob 120 | 121 | 122 | if __name__ == '__main__': 123 | pytest.main() 124 | -------------------------------------------------------------------------------- /tests/test_constants.py: -------------------------------------------------------------------------------- 1 | # vim: set fileencoding=utf-8 : 2 | 3 | import pyvips 4 | 5 | 6 | class TestConstants: 7 | def test_2Darray(self): 8 | im = pyvips.Image.new_from_array([[1, 2, 3, 4], [5, 6, 7, 8]]) 9 | assert im.width == 4 10 | assert im.height == 2 11 | 12 | def test_1Darray(self): 13 | im = pyvips.Image.new_from_array([1, 2, 3, 4]) 14 | assert im.width == 4 15 | assert im.height == 1 16 | 17 | def test_array_const_args(self): 18 | black = pyvips.Image.black(16, 16) 19 | r = black.draw_rect(255, 10, 12, 1, 1) 20 | g = black.draw_rect(255, 10, 11, 1, 1) 21 | b = black.draw_rect(255, 10, 10, 1, 1) 22 | im = r.bandjoin([g, b]) 23 | 24 | assert im.width == 16 25 | assert im.height == 16 26 | assert im.bands == 3 27 | 28 | im = im.conv([ 29 | [0.11, 0.11, 0.11], 30 | [0.11, 0.11, 0.11], 31 | [0.11, 0.11, 0.11] 32 | ]) 33 | 34 | assert im.width == 16 35 | assert im.height == 16 36 | assert im.bands == 3 37 | 38 | def test_scale_offset(self): 39 | im = pyvips.Image.new_from_array([1, 2], 8, 2) 40 | 41 | assert im.width == 2 42 | assert im.height == 1 43 | assert im.bands == 1 44 | assert im.scale == 8 45 | assert im.offset == 2 46 | assert im.avg() == 1.5 47 | 48 | def test_binary_scalar(self): 49 | im = pyvips.Image.black(16, 16) + 128 50 | 51 | im += 128 52 | im -= 128 53 | im *= 2 54 | im /= 2 55 | im %= 100 56 | im += 100 57 | im **= 2 58 | im **= 0.5 59 | im <<= 1 60 | im >>= 1 61 | im |= 64 62 | im &= 32 63 | im ^= 128 64 | 65 | assert im.avg() == 128 66 | 67 | def test_binary_vector(self): 68 | im = pyvips.Image.black(16, 16, bands=3) + 128 69 | 70 | im += [128, 0, 0] 71 | im -= [128, 0, 0] 72 | im *= [2, 1, 1] 73 | im /= [2, 1, 1] 74 | im %= [100, 99, 98] 75 | im += [100, 99, 98] 76 | im **= [2, 3, 4] 77 | im **= [1.0 / 2.0, 1.0 / 3.0, 1.0 / 4.0] 78 | im <<= [1, 2, 3] 79 | im >>= [1, 2, 3] 80 | im |= [64, 128, 256] 81 | im &= [64, 128, 256] 82 | im ^= [64 + 128, 0, 256 + 128] 83 | 84 | assert im.avg() == 128 85 | 86 | def test_binary_image(self): 87 | im = pyvips.Image.black(16, 16) + 128 88 | x = im 89 | 90 | x += im 91 | x -= im 92 | x *= im 93 | x /= im 94 | x %= im 95 | x += im 96 | x |= im 97 | x &= im 98 | x ^= im 99 | 100 | assert x.avg() == 0 101 | 102 | def test_binary_relational_scalar(self): 103 | im = pyvips.Image.black(16, 16) + 128 104 | 105 | assert (im > 128).avg() == 0 106 | assert (im >= 128).avg() == 255 107 | assert (im < 128).avg() == 0 108 | assert (im <= 128).avg() == 255 109 | assert (im == 128).avg() == 255 110 | assert (im != 128).avg() == 0 111 | 112 | def test_binary_relational_vector(self): 113 | im = pyvips.Image.black(16, 16, bands=3) + [100, 128, 130] 114 | 115 | assert (im > [100, 128, 130]).avg() == 0 116 | assert (im >= [100, 128, 130]).avg() == 255 117 | assert (im < [100, 128, 130]).avg() == 0 118 | assert (im <= [100, 128, 130]).avg() == 255 119 | assert (im == [100, 128, 130]).avg() == 255 120 | assert (im != [100, 128, 130]).avg() == 0 121 | 122 | def test_binary_relational_image(self): 123 | im = pyvips.Image.black(16, 16) + 128 124 | 125 | assert (im > im).avg() == 0 126 | assert (im >= im).avg() == 255 127 | assert (im < im).avg() == 0 128 | assert (im <= im).avg() == 255 129 | assert (im == im).avg() == 255 130 | assert (im != im).avg() == 0 131 | 132 | def test_band_extract_scalar(self): 133 | im = pyvips.Image.black(16, 16, bands=3) + [100, 128, 130] 134 | x = im[1] 135 | 136 | assert x.width == 16 137 | assert x.height == 16 138 | assert x.bands == 1 139 | assert x.avg() == 128 140 | 141 | def test_band_extract_slice(self): 142 | im = pyvips.Image.black(16, 16, bands=3) + [100, 128, 130] 143 | x = im[1:3] 144 | 145 | assert x.width == 16 146 | assert x.height == 16 147 | assert x.bands == 2 148 | assert x.avg() == 129 149 | -------------------------------------------------------------------------------- /tests/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 16 | 18 | image/svg+xml 19 | 21 | 22 | 23 | 24 | 25 | 27 | 31 | 32 | -------------------------------------------------------------------------------- /pyvips/vobject.py: -------------------------------------------------------------------------------- 1 | # wrap VipsObject 2 | 3 | import logging 4 | 5 | import pyvips 6 | from pyvips import ffi, vips_lib, gobject_lib, Error, _to_bytes, _to_string 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | class VipsObject(pyvips.GObject): 12 | """Manage a VipsObject.""" 13 | __slots__ = ('vobject', 'gobject') 14 | _pspec_cache = {} 15 | 16 | def __init__(self, pointer): 17 | # logger.debug('VipsObject.__init__: pointer = %s', pointer) 18 | super(VipsObject, self).__init__(pointer) 19 | self.vobject = ffi.cast('VipsObject*', pointer) 20 | self.gobject = ffi.cast('GObject*', pointer) 21 | 22 | @staticmethod 23 | def print_all(): 24 | """Print all objects. 25 | 26 | Print a table of all active libvips objects. Handy for debugging. 27 | 28 | """ 29 | vips_lib.vips_object_print_all() 30 | 31 | def _get_pspec(self, name): 32 | # logger.debug('VipsObject.get_typeof: self = %s, name = %s', 33 | # str(self), name) 34 | 35 | # this is pretty slow, and used a lot, so we cache results 36 | # this cache makes the libvips test suite about 10% faster 37 | class_pointer = self.gobject.g_type_instance.g_class 38 | cache = VipsObject._pspec_cache 39 | if class_pointer not in cache: 40 | cache[class_pointer] = {} 41 | if name not in cache[class_pointer]: 42 | pspec = ffi.new('GParamSpec **') 43 | argument_class = ffi.new('VipsArgumentClass **') 44 | argument_instance = ffi.new('VipsArgumentInstance **') 45 | result = vips_lib.vips_object_get_argument(self.vobject, 46 | _to_bytes(name), 47 | pspec, argument_class, 48 | argument_instance) 49 | 50 | if result != 0: 51 | return None 52 | 53 | cache[class_pointer][name] = pspec[0] 54 | 55 | return cache[class_pointer][name] 56 | 57 | def get_typeof(self, name): 58 | """Get the GType of a GObject property. 59 | 60 | This function returns 0 if the property does not exist. 61 | 62 | """ 63 | 64 | # logger.debug('VipsObject.get_typeof: self = %s, name = %s', 65 | # str(self), name) 66 | 67 | pspec = self._get_pspec(name) 68 | if pspec is None: 69 | # need to clear any error, this is horrible 70 | Error('') 71 | return 0 72 | 73 | return pspec.value_type 74 | 75 | def get_blurb(self, name): 76 | """Get the blurb for a GObject property.""" 77 | 78 | c_str = gobject_lib.g_param_spec_get_blurb(self._get_pspec(name)) 79 | return _to_string(c_str) 80 | 81 | def get(self, name): 82 | """Get a GObject property. 83 | 84 | The value of the property is converted to a Python value. 85 | 86 | """ 87 | 88 | logger.debug('VipsObject.get: name = %s', name) 89 | 90 | pspec = self._get_pspec(name) 91 | if pspec is None: 92 | raise Error('Property not found.') 93 | gtype = pspec.value_type 94 | 95 | gv = pyvips.GValue() 96 | gv.set_type(gtype) 97 | go = ffi.cast('GObject *', self.pointer) 98 | gobject_lib.g_object_get_property(go, _to_bytes(name), gv.pointer) 99 | 100 | return gv.get() 101 | 102 | def set(self, name, value): 103 | """Set a GObject property. 104 | 105 | The value is converted to the property type, if possible. 106 | 107 | """ 108 | 109 | logger.debug('VipsObject.set: name = %s, value = %s', name, value) 110 | 111 | gtype = self.get_typeof(name) 112 | 113 | gv = pyvips.GValue() 114 | gv.set_type(gtype) 115 | gv.set(value) 116 | go = ffi.cast('GObject *', self.pointer) 117 | gobject_lib.g_object_set_property(go, _to_bytes(name), gv.pointer) 118 | 119 | def set_string(self, string_options): 120 | """Set a series of properties using a string. 121 | 122 | For example:: 123 | 124 | 'fred=12, tile' 125 | '[fred=12]' 126 | 127 | """ 128 | 129 | cstr = _to_bytes(string_options) 130 | result = vips_lib.vips_object_set_from_string(self.vobject, cstr) 131 | 132 | return result == 0 133 | 134 | def get_description(self): 135 | """Get the description of a GObject.""" 136 | 137 | return _to_string(vips_lib.vips_object_get_description(self.vobject)) 138 | 139 | 140 | __all__ = ['VipsObject'] 141 | -------------------------------------------------------------------------------- /pyvips/base.py: -------------------------------------------------------------------------------- 1 | # basic defs and link to ffi 2 | 3 | 4 | from pyvips import ffi, glib_lib, vips_lib, gobject_lib, \ 5 | _to_string, _to_bytes, Error 6 | 7 | 8 | def leak_set(leak): 9 | """Enable or disable libvips leak checking. 10 | 11 | With this enabled, libvips will check for object and area leaks on exit. 12 | Enabling this option will make libvips run slightly more slowly. 13 | """ 14 | 15 | return vips_lib.vips_leak_set(leak) 16 | 17 | 18 | def shutdown(): 19 | """Shut libvips down.""" 20 | vips_lib.vips_shutdown() 21 | 22 | 23 | def version(flag): 24 | """Get the major, minor or micro version number of the libvips library. 25 | 26 | Args: 27 | flag (int): Pass flag 0 to get the major version number, flag 1 to 28 | get minor, flag 2 to get micro. 29 | 30 | Returns: 31 | The version number, 32 | 33 | Raises: 34 | :class:`.Error` 35 | """ 36 | 37 | value = vips_lib.vips_version(flag) 38 | if value < 0: 39 | raise Error('unable to get library version') 40 | 41 | return value 42 | 43 | 44 | def get_suffixes(): 45 | """Get a list of all the filename suffixes supported by libvips. 46 | 47 | Returns: 48 | [string] 49 | 50 | """ 51 | 52 | names = [] 53 | 54 | if at_least_libvips(8, 8): 55 | array = vips_lib.vips_foreign_get_suffixes() 56 | i = 0 57 | while array[i] != ffi.NULL: 58 | name = _to_string(array[i]) 59 | if name not in names: 60 | names.append(name) 61 | glib_lib.g_free(array[i]) 62 | i += 1 63 | glib_lib.g_free(array) 64 | 65 | return names 66 | 67 | 68 | # we need to define this before we import the declarations: they need to know 69 | # which bits to make 70 | def at_least_libvips(x, y): 71 | """Is this at least libvips x.y?""" 72 | 73 | major = version(0) 74 | minor = version(1) 75 | 76 | return major > x or (major == x and minor >= y) 77 | 78 | 79 | def type_find(basename, nickname): 80 | """Get the GType for a name. 81 | 82 | Looks up the GType for a nickname. Types below basename in the type 83 | hierarchy are searched. 84 | """ 85 | 86 | return vips_lib.vips_type_find(_to_bytes(basename), _to_bytes(nickname)) 87 | 88 | 89 | def type_name(gtype): 90 | """Return the name for a GType.""" 91 | 92 | return _to_string(gobject_lib.g_type_name(gtype)) 93 | 94 | 95 | def nickname_find(gtype): 96 | """Return the nickname for a GType.""" 97 | 98 | return _to_string(vips_lib.vips_nickname_find(gtype)) 99 | 100 | 101 | def type_from_name(name): 102 | """Return the GType for a name.""" 103 | 104 | return gobject_lib.g_type_from_name(_to_bytes(name)) 105 | 106 | 107 | def type_map(gtype, fn): 108 | """Map fn over all child types of gtype.""" 109 | 110 | cb = ffi.callback('VipsTypeMap2Fn', fn) 111 | return vips_lib.vips_type_map(gtype, cb, ffi.NULL, ffi.NULL) 112 | 113 | 114 | def values_for_enum(gtype): 115 | """Deprecated.""" 116 | 117 | g_type_class = gobject_lib.g_type_class_ref(gtype) 118 | g_enum_class = ffi.cast('GEnumClass *', g_type_class) 119 | 120 | return [_to_string(g_enum_class.values[i].value_nick) 121 | for i in range(g_enum_class.n_values)] 122 | 123 | 124 | def values_for_flag(gtype): 125 | """Deprecated.""" 126 | 127 | g_type_class = gobject_lib.g_type_class_ref(gtype) 128 | g_flags_class = ffi.cast('GFlagsClass *', g_type_class) 129 | 130 | return [_to_string(g_flags_class.values[i].value_nick) 131 | for i in range(g_flags_class.n_values)] 132 | 133 | 134 | def enum_dict(gtype): 135 | """Get name -> value dict for a enum (gtype).""" 136 | 137 | g_type_class = gobject_lib.g_type_class_ref(gtype) 138 | g_enum_class = ffi.cast('GEnumClass *', g_type_class) 139 | 140 | return {_to_string(g_enum_class.values[i].value_nick): 141 | g_enum_class.values[i].value 142 | for i in range(g_enum_class.n_values)} 143 | 144 | 145 | def flags_dict(gtype): 146 | """Get name -> value dict for a flags (gtype).""" 147 | 148 | g_type_class = gobject_lib.g_type_class_ref(gtype) 149 | g_flags_class = ffi.cast('GFlagsClass *', g_type_class) 150 | 151 | return {_to_string(g_flags_class.values[i].value_nick): 152 | g_flags_class.values[i].value 153 | for i in range(g_flags_class.n_values)} 154 | 155 | 156 | __all__ = [ 157 | 'leak_set', 158 | 'shutdown', 159 | 'version', 160 | 'at_least_libvips', 161 | 'type_find', 162 | 'nickname_find', 163 | 'get_suffixes', 164 | 'type_name', 165 | 'type_map', 166 | 'type_from_name', 167 | 'values_for_enum', 168 | 'values_for_flag', 169 | 'enum_dict', 170 | 'flags_dict' 171 | ] 172 | -------------------------------------------------------------------------------- /tests/helpers/helpers.py: -------------------------------------------------------------------------------- 1 | # vim: set fileencoding=utf-8 : 2 | # test helpers 3 | import os 4 | import tempfile 5 | import pytest 6 | 7 | import pyvips 8 | 9 | IMAGES = os.path.join(os.path.dirname(__file__), os.pardir, 'images') 10 | JPEG_FILE = os.path.join(IMAGES, "sample.jpg") 11 | WEBP_FILE = os.path.join(IMAGES, "sample.webp") 12 | SVG_FILE = os.path.join(IMAGES, "logo.svg") 13 | UHDR_FILE = os.path.join(IMAGES, "ultra-hdr.jpg") 14 | 15 | 16 | # an expanding zip ... if either of the args is a scalar or a one-element list, 17 | # duplicate it down the other side 18 | def zip_expand(x, y): 19 | # handle singleton list case 20 | if isinstance(x, list) and len(x) == 1: 21 | x = x[0] 22 | if isinstance(y, list) and len(y) == 1: 23 | y = y[0] 24 | 25 | if isinstance(x, list) and isinstance(y, list): 26 | return list(zip(x, y)) 27 | elif isinstance(x, list): 28 | return [[i, y] for i in x] 29 | elif isinstance(y, list): 30 | return [[x, j] for j in y] 31 | else: 32 | return [[x, y]] 33 | 34 | 35 | # run a 1-ary function on a thing -- loop over elements if the 36 | # thing is a list 37 | def run_fn(fn, x): 38 | if isinstance(x, list): 39 | return [fn(i) for i in x] 40 | else: 41 | return fn(x) 42 | 43 | 44 | # make a temp filename with the specified suffix and in the 45 | # specified directory 46 | def temp_filename(directory, suffix): 47 | temp_name = next(tempfile._get_candidate_names()) 48 | filename = os.path.join(directory, temp_name + suffix) 49 | 50 | return filename 51 | 52 | 53 | # test for an operator exists 54 | def have(name): 55 | return pyvips.type_find("VipsOperation", name) != 0 56 | 57 | 58 | # use as @skip_if_no('jpegload') 59 | def skip_if_no(operator_name): 60 | return pytest.mark.skipif(not have(operator_name), 61 | reason=('no {}, skipping test'. 62 | format(operator_name))) 63 | 64 | 65 | # run a 2-ary function on two things -- loop over elements pairwise if the 66 | # things are lists 67 | def run_fn2(fn, x, y): 68 | if isinstance(x, pyvips.Image) or isinstance(y, pyvips.Image): 69 | return fn(x, y) 70 | elif isinstance(x, list) or isinstance(y, list): 71 | return [fn(i, j) for i, j in zip_expand(x, y)] 72 | else: 73 | return fn(x, y) 74 | 75 | 76 | # test a pair of things which can be lists for approx. equality 77 | def assert_almost_equal_objects(a, b, threshold=0.0001, msg=''): 78 | # print(f'assertAlmostEqualObjects {a} = {b}') 79 | assert all([pytest.approx(x, abs=threshold) == y 80 | for x, y in zip_expand(a, b)]), msg 81 | 82 | 83 | # test a pair of things which can be lists for equality 84 | def assert_equal_objects(a, b, msg=''): 85 | # print(f'assertEqualObjects {a} = {b}') 86 | assert all([x == y for x, y in zip_expand(a, b)]), msg 87 | 88 | 89 | # test a pair of things which can be lists for difference less than a 90 | # threshold 91 | def assert_less_threshold(a, b, diff): 92 | assert all([abs(x - y) < diff for x, y in zip_expand(a, b)]) 93 | 94 | 95 | # run a function on an image and on a single pixel, the results 96 | # should match 97 | def run_cmp(message, im, x, y, fn): 98 | a = im(x, y) 99 | v1 = fn(a) 100 | im2 = fn(im) 101 | v2 = im2(x, y) 102 | assert_almost_equal_objects(v1, v2, msg=message) 103 | 104 | 105 | # run a function on an image, 106 | # 50,50 and 10,10 should have different values on the test image 107 | def run_image(message, im, fn): 108 | run_cmp(message, im, 50, 50, fn) 109 | run_cmp(message, im, 10, 10, fn) 110 | 111 | 112 | # run a function on (image, constant), and on (constant, image). 113 | # 50,50 and 10,10 should have different values on the test image 114 | def run_const(message, fn, im, c): 115 | run_cmp(message, im, 50, 50, lambda x: run_fn2(fn, x, c)) 116 | run_cmp(message, im, 50, 50, lambda x: run_fn2(fn, c, x)) 117 | run_cmp(message, im, 10, 10, lambda x: run_fn2(fn, x, c)) 118 | run_cmp(message, im, 10, 10, lambda x: run_fn2(fn, c, x)) 119 | 120 | 121 | # run a function on a pair of images and on a pair of pixels, the results 122 | # should match 123 | def run_cmp2(message, left, right, x, y, fn): 124 | a = left(x, y) 125 | b = right(x, y) 126 | v1 = fn(a, b) 127 | after = fn(left, right) 128 | v2 = after(x, y) 129 | assert_almost_equal_objects(v1, v2, msg=message) 130 | 131 | 132 | # run a function on a pair of images 133 | # 50,50 and 10,10 should have different values on the test image 134 | def run_image2(message, left, right, fn): 135 | run_cmp2(message, left, right, 50, 50, 136 | lambda x, y: run_fn2(fn, x, y)) 137 | run_cmp2(message, left, right, 10, 10, 138 | lambda x, y: run_fn2(fn, x, y)) 139 | -------------------------------------------------------------------------------- /examples/gen-enums.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | import xml.etree.ElementTree as ET 5 | 6 | from pyvips import ffi, enum_dict, flags_dict, \ 7 | type_map, type_name, type_from_name 8 | 9 | # This file generates enums.py -- the set of classes giving the permissible 10 | # values for the pyvips enums/flags. Run with something like: 11 | # 12 | # ./gen-enums.py ~/GIT/libvips/build/libvips/Vips-8.0.gir > enums.py 13 | # mv enums.py ../pyvips 14 | 15 | # The GIR file 16 | root = ET.parse(sys.argv[1]).getroot() 17 | namespace = { 18 | "goi": "http://www.gtk.org/introspection/core/1.0" 19 | } 20 | 21 | # find all the enumerations/flags and make a dict for them 22 | xml_enums = {} 23 | for node in root.findall("goi:namespace/goi:enumeration", namespace): 24 | xml_enums[node.get('name')] = node 25 | 26 | xml_flags = {} 27 | for node in root.findall("goi:namespace/goi:bitfield", namespace): 28 | xml_flags[node.get('name')] = node 29 | 30 | 31 | def remove_prefix(enum_str): 32 | prefix = 'Vips' 33 | 34 | if enum_str.startswith(prefix): 35 | return enum_str[len(prefix):] 36 | 37 | return enum_str 38 | 39 | 40 | def rewrite_references(string): 41 | """Rewrite a gi-docgen references to RST style. 42 | 43 | gi-docgen references look like this: 44 | 45 | [func@version] 46 | [class@Image] 47 | [func@Image.bandjoin] 48 | [meth@Image.add] 49 | [ctor@Image.new_from_file] 50 | [enum@SdfShape] 51 | [enum@Vips.SdfShape.CIRCLE] 52 | 53 | we look for the approximate patterns and rewrite in RST style, so: 54 | 55 | :meth:`.version` 56 | :class:`.Image` 57 | :meth:`.Image.bandjoin` 58 | :meth:`.Image.new_from_file` 59 | :class:`.enums.SdfShape` 60 | :class:`.enums.SdfShape.CIRCLE` 61 | """ 62 | 63 | import re 64 | while True: 65 | match = re.search(r"\[(.*?)@(.*?)\]", string) 66 | if not match: 67 | break 68 | 69 | before = string[0:match.span(0)[0]] 70 | type = match[1] 71 | target = match[2] 72 | after = string[match.span(0)[1]:] 73 | 74 | if type in ["ctor", "meth", "method", "func"]: 75 | python_type = "meth" 76 | elif type in ["class", "enum", "flags"]: 77 | python_type = "class" 78 | else: 79 | raise Exception(f'type "{type}" is unknown') 80 | 81 | match = re.match("Vips.(.*)", target) 82 | if match: 83 | target = match[1] 84 | 85 | if type == "enum": 86 | target = f"enums.{target}" 87 | 88 | string = f"{before}:{python_type}:`.{target}`{after}" 89 | 90 | return string 91 | 92 | 93 | def generate_enums(): 94 | all_nicknames = [] 95 | 96 | def add_nickname(gtype, a, b): 97 | nickname = type_name(gtype) 98 | all_nicknames.append(nickname) 99 | 100 | type_map(gtype, add_nickname) 101 | 102 | return ffi.NULL 103 | 104 | type_map(type_from_name('GEnum'), add_nickname) 105 | 106 | # Filter internal enums 107 | blacklist = ['VipsDemandStyle'] 108 | all_nicknames = [name for name in all_nicknames if name not in blacklist] 109 | 110 | for name in all_nicknames: 111 | gtype = type_from_name(name) 112 | python_name = remove_prefix(name) 113 | if python_name not in xml_enums: 114 | continue 115 | 116 | node = xml_enums[python_name] 117 | values = enum_dict(gtype) 118 | enum_doc = node.find("goi:doc", namespace) 119 | 120 | print('') 121 | print('') 122 | print(f'class {python_name}(object):') 123 | print(f' """{python_name}.') 124 | if enum_doc is not None: 125 | print('') 126 | print(f'{rewrite_references(enum_doc.text)}') 127 | print('') 128 | print('Attributes:') 129 | print('') 130 | for key, value in values.items(): 131 | python_name = key.replace('-', '_') 132 | member = node.find(f"goi:member[@name='{python_name}']", namespace) 133 | member_doc = member.find("goi:doc", namespace) 134 | if member_doc is not None: 135 | text = rewrite_references(member_doc.text) 136 | print(f' {python_name.upper()} (str): {text}') 137 | print('') 138 | print(' """') 139 | print('') 140 | 141 | for key, value in values.items(): 142 | python_name = key.replace('-', '_').upper() 143 | print(f' {python_name} = \'{key}\'') 144 | 145 | 146 | def generate_flags(): 147 | all_nicknames = [] 148 | 149 | def add_nickname(gtype, a, b): 150 | nickname = type_name(gtype) 151 | all_nicknames.append(nickname) 152 | 153 | type_map(gtype, add_nickname) 154 | 155 | return ffi.NULL 156 | 157 | type_map(type_from_name('GFlags'), add_nickname) 158 | 159 | # Filter internal flags 160 | blacklist = ['VipsForeignFlags'] 161 | all_nicknames = [name for name in all_nicknames if name not in blacklist] 162 | 163 | for name in all_nicknames: 164 | gtype = type_from_name(name) 165 | python_name = remove_prefix(name) 166 | if python_name not in xml_flags: 167 | continue 168 | 169 | node = xml_flags[python_name] 170 | values = flags_dict(gtype) 171 | enum_doc = node.find("goi:doc", namespace) 172 | 173 | print('') 174 | print('') 175 | print(f'class {python_name}(object):') 176 | print(f' """{python_name}.') 177 | if enum_doc is not None: 178 | print('') 179 | print(f'{rewrite_references(enum_doc.text)}') 180 | print('') 181 | print('Attributes:') 182 | print('') 183 | for key, value in values.items(): 184 | python_name = key.replace('-', '_') 185 | member = node.find(f"goi:member[@name='{python_name}']", namespace) 186 | member_doc = member.find("goi:doc", namespace) 187 | if member_doc is not None: 188 | text = member_doc.text 189 | print(f' {python_name.upper()} (int): ' 190 | f'{rewrite_references(text)}') 191 | print('') 192 | print(' """') 193 | print('') 194 | 195 | for key, value in values.items(): 196 | python_name = key.replace('-', '_').upper() 197 | print(f' {python_name} = {value}') 198 | 199 | 200 | if __name__ == "__main__": 201 | print('# libvips enums -- this file is generated automatically') 202 | print('# flake8: noqa: E501') # ignore line too long error 203 | generate_enums() 204 | generate_flags() 205 | -------------------------------------------------------------------------------- /tests/test_connections.py: -------------------------------------------------------------------------------- 1 | # vim: set fileencoding=utf-8 : 2 | 3 | import tempfile 4 | import pytest 5 | 6 | import pyvips 7 | from helpers import JPEG_FILE, WEBP_FILE, temp_filename, skip_if_no 8 | 9 | 10 | class TestConnections: 11 | @classmethod 12 | def setup_class(cls): 13 | cls.tempdir = tempfile.mkdtemp() 14 | 15 | @skip_if_no('jpegload') 16 | @pytest.mark.skipif(not pyvips.at_least_libvips(8, 9), 17 | reason="requires libvips >= 8.9") 18 | def test_connection(self): 19 | source = pyvips.Source.new_from_file(JPEG_FILE) 20 | image = pyvips.Image.new_from_source(source, '', access='sequential') 21 | filename = temp_filename(self.tempdir, '.png') 22 | target = pyvips.Target.new_to_file(filename) 23 | image.write_to_target(target, '.png') 24 | 25 | image = pyvips.Image.new_from_file(JPEG_FILE, access='sequential') 26 | image2 = pyvips.Image.new_from_file(filename, access='sequential') 27 | 28 | assert (image - image2).abs().max() < 10 29 | 30 | @skip_if_no('jpegload') 31 | @pytest.mark.skipif(not pyvips.at_least_libvips(8, 9), 32 | reason="requires libvips >= 8.9") 33 | def test_source_custom_no_seek(self): 34 | input_file = open(JPEG_FILE, "rb") 35 | 36 | def read_handler(size): 37 | return input_file.read(size) 38 | 39 | source = pyvips.SourceCustom() 40 | source.on_read(read_handler) 41 | 42 | image = pyvips.Image.new_from_source(source, '', access='sequential') 43 | image2 = pyvips.Image.new_from_file(JPEG_FILE, access='sequential') 44 | 45 | assert (image - image2).abs().max() == 0 46 | 47 | @skip_if_no('jpegload') 48 | @pytest.mark.skipif(not pyvips.at_least_libvips(8, 9), 49 | reason="requires libvips >= 8.9") 50 | def test_source_custom(self): 51 | input_file = open(JPEG_FILE, "rb") 52 | 53 | def read_handler(size): 54 | return input_file.read(size) 55 | 56 | def seek_handler(offset, whence): 57 | input_file.seek(offset, whence) 58 | return input_file.tell() 59 | 60 | source = pyvips.SourceCustom() 61 | source.on_read(read_handler) 62 | source.on_seek(seek_handler) 63 | 64 | image = pyvips.Image.new_from_source(source, '', 65 | access='sequential') 66 | image2 = pyvips.Image.new_from_file(JPEG_FILE, access='sequential') 67 | 68 | assert (image - image2).abs().max() == 0 69 | 70 | @skip_if_no('jpegload') 71 | @pytest.mark.skipif(not pyvips.at_least_libvips(8, 9), 72 | reason="requires libvips >= 8.9") 73 | def test_target_custom(self): 74 | filename = temp_filename(self.tempdir, '.png') 75 | output_file = open(filename, "w+b") 76 | 77 | def write_handler(chunk): 78 | return output_file.write(chunk) 79 | 80 | def end_handler(): 81 | try: 82 | output_file.close() 83 | except IOError: 84 | return -1 85 | else: 86 | return 0 87 | 88 | target = pyvips.TargetCustom() 89 | target.on_write(write_handler) 90 | target.on_end(end_handler) 91 | 92 | image = pyvips.Image.new_from_file(JPEG_FILE, access='sequential') 93 | image.write_to_target(target, '.png') 94 | 95 | image = pyvips.Image.new_from_file(JPEG_FILE, access='sequential') 96 | image2 = pyvips.Image.new_from_file(filename, access='sequential') 97 | 98 | assert (image - image2).abs().max() == 0 99 | 100 | @skip_if_no('jpegload') 101 | @skip_if_no('tiffsave') 102 | @pytest.mark.skipif(not pyvips.at_least_libvips(8, 13), 103 | reason="requires libvips >= 8.13") 104 | def test_target_custom_seek(self): 105 | filename = temp_filename(self.tempdir, '.png') 106 | output_file = open(filename, "w+b") 107 | 108 | def write_handler(chunk): 109 | return output_file.write(chunk) 110 | 111 | def read_handler(size): 112 | return output_file.read(size) 113 | 114 | def seek_handler(offset, whence): 115 | output_file.seek(offset, whence) 116 | return output_file.tell() 117 | 118 | def end_handler(): 119 | try: 120 | output_file.close() 121 | except IOError: 122 | return -1 123 | else: 124 | return 0 125 | 126 | target = pyvips.TargetCustom() 127 | target.on_write(write_handler) 128 | target.on_read(read_handler) 129 | target.on_seek(seek_handler) 130 | target.on_end(end_handler) 131 | 132 | image = pyvips.Image.new_from_file(JPEG_FILE, access='sequential') 133 | image.write_to_target(target, '.tif') 134 | 135 | image = pyvips.Image.new_from_file(JPEG_FILE, access='sequential') 136 | image2 = pyvips.Image.new_from_file(filename, access='sequential') 137 | 138 | assert (image - image2).abs().max() == 0 139 | 140 | # test webp as well, since that maps the stream rather than using read 141 | 142 | @skip_if_no('webpload') 143 | @pytest.mark.skipif(not pyvips.at_least_libvips(8, 9), 144 | reason="requires libvips >= 8.9") 145 | def test_source_custom_webp_no_seek(self): 146 | input_file = open(WEBP_FILE, "rb") 147 | 148 | def read_handler(size): 149 | return input_file.read(size) 150 | 151 | source = pyvips.SourceCustom() 152 | source.on_read(read_handler) 153 | 154 | image = pyvips.Image.new_from_source(source, '', 155 | access='sequential') 156 | image2 = pyvips.Image.new_from_file(WEBP_FILE, access='sequential') 157 | 158 | assert (image - image2).abs().max() == 0 159 | 160 | @skip_if_no('webpload') 161 | @pytest.mark.skipif(not pyvips.at_least_libvips(8, 9), 162 | reason="requires libvips >= 8.9") 163 | def test_source_custom_webp(self): 164 | input_file = open(WEBP_FILE, "rb") 165 | 166 | def read_handler(size): 167 | return input_file.read(size) 168 | 169 | def seek_handler(offset, whence): 170 | input_file.seek(offset, whence) 171 | return input_file.tell() 172 | 173 | source = pyvips.SourceCustom() 174 | source.on_read(read_handler) 175 | source.on_seek(seek_handler) 176 | 177 | image = pyvips.Image.new_from_source(source, '', 178 | access='sequential') 179 | image2 = pyvips.Image.new_from_file(WEBP_FILE, access='sequential') 180 | 181 | assert (image - image2).abs().max() == 0 182 | -------------------------------------------------------------------------------- /pyvips/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | 3 | import logging 4 | import os 5 | import sys 6 | import atexit 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | # user code can override this null handler 11 | logger.addHandler(logging.NullHandler()) 12 | 13 | def library_name(name, abi_number, lib_prefix='lib'): 14 | is_windows = os.name == 'nt' 15 | is_mac = sys.platform == 'darwin' 16 | 17 | if is_windows: 18 | return f'{lib_prefix}{name}-{abi_number}.dll' 19 | elif is_mac: 20 | return f'{lib_prefix}{name}.{abi_number}.dylib' 21 | else: 22 | return f'{lib_prefix}{name}.so.{abi_number}' 23 | 24 | # pull in our module version number 25 | from .version import __version__ 26 | 27 | # try to import our binary interface ... if that works, we are in API mode 28 | API_mode = False 29 | try: 30 | import _libvips 31 | 32 | logger.debug('Loaded binary module _libvips') 33 | 34 | ffi = _libvips.ffi 35 | vips_lib = _libvips.lib 36 | glib_lib = _libvips.lib 37 | gobject_lib = _libvips.lib 38 | 39 | # now check that the binary wrapper is for the same version of libvips that 40 | # we find ourseleves linking to at runtime ... if it isn't, we must fall 41 | # back to ABI mode 42 | lib_major = vips_lib.vips_version(0) 43 | lib_minor = vips_lib.vips_version(1) 44 | wrap_major = vips_lib.VIPS_MAJOR_VERSION 45 | wrap_minor = vips_lib.VIPS_MINOR_VERSION 46 | logger.debug(f'Module generated for libvips {wrap_major}.{wrap_minor}') 47 | logger.debug(f'Linked to libvips {lib_major}.{lib_minor}') 48 | 49 | if wrap_major != lib_major or wrap_minor != lib_minor: 50 | raise Exception('bad wrapper version') 51 | 52 | API_mode = True 53 | 54 | except Exception as e: 55 | logger.debug(f'Binary module load failed: {e}') 56 | logger.debug('Falling back to ABI mode') 57 | 58 | from cffi import FFI 59 | 60 | ffi = FFI() 61 | 62 | vips_lib = ffi.dlopen(library_name('vips', 42)) 63 | glib_lib = vips_lib 64 | gobject_lib = vips_lib 65 | 66 | logger.debug('Loaded lib %s', vips_lib) 67 | 68 | ffi.cdef(''' 69 | int vips_init (const char* argv0); 70 | int vips_version (int flag); 71 | ''') 72 | 73 | if vips_lib.vips_init(sys.argv[0].encode()) != 0: 74 | raise Exception('unable to init libvips') 75 | 76 | logger.debug('Inited libvips') 77 | 78 | if not API_mode: 79 | from .vdecls import cdefs 80 | 81 | major = vips_lib.vips_version(0) 82 | minor = vips_lib.vips_version(1) 83 | micro = vips_lib.vips_version(2) 84 | features = { 85 | 'major': major, 86 | 'minor': minor, 87 | 'micro': micro, 88 | 'api': False, 89 | } 90 | 91 | ffi.cdef(cdefs(features)) 92 | 93 | # We can sometimes get dependent libraries from libvips -- either the platform 94 | # will open dependencies for us automatically, or the libvips binary has been 95 | # built to includes all main dependencies (common on windows, can happen 96 | # elsewhere). 97 | # 98 | # We must get glib functions from libvips if we can, since it will be the 99 | # one that libvips itself is using, and they will share runtime types. 100 | try: 101 | is_unified = gobject_lib.g_type_from_name(b'VipsImage') != 0 102 | except Exception: 103 | is_unified = False 104 | 105 | if not is_unified: 106 | try: 107 | glib_lib = ffi.dlopen(library_name('glib-2.0', 0)) 108 | gobject_lib = ffi.dlopen(library_name('gobject-2.0', 0)) 109 | except Exception: 110 | # on windows glib maybe named glib-2.0-0.dll instead of libglib-2.0-0.dll 111 | glib_lib = ffi.dlopen(library_name('glib-2.0', 0, lib_prefix='')) 112 | gobject_lib = ffi.dlopen(library_name('gobject-2.0', 0, lib_prefix='')) 113 | 114 | logger.debug('Loaded lib %s', glib_lib) 115 | logger.debug('Loaded lib %s', gobject_lib) 116 | 117 | 118 | from .error import * 119 | 120 | # redirect all vips warnings to logging 121 | 122 | class GLogLevelFlags(object): 123 | # log flags 124 | FLAG_RECURSION = 1 << 0 125 | FLAG_FATAL = 1 << 1 126 | 127 | # GLib log levels 128 | LEVEL_ERROR = 1 << 2 # always fatal 129 | LEVEL_CRITICAL = 1 << 3 130 | LEVEL_WARNING = 1 << 4 131 | LEVEL_MESSAGE = 1 << 5 132 | LEVEL_INFO = 1 << 6 133 | LEVEL_DEBUG = 1 << 7 134 | 135 | LEVEL_TO_LOGGER = { 136 | LEVEL_DEBUG: 10, 137 | LEVEL_INFO: 20, 138 | LEVEL_MESSAGE: 20, 139 | LEVEL_WARNING: 30, 140 | LEVEL_ERROR: 40, 141 | LEVEL_CRITICAL: 50, 142 | } 143 | 144 | 145 | if API_mode: 146 | @ffi.def_extern() 147 | def _log_handler_callback(domain, level, message, user_data): 148 | logger.log(GLogLevelFlags.LEVEL_TO_LOGGER[level], 149 | f'{_to_string(domain)}: {_to_string(message)}') 150 | 151 | # keep a ref to the cb to stop it being GCd 152 | _log_handler_cb = glib_lib._log_handler_callback 153 | else: 154 | def _log_handler_callback(domain, level, message, user_data): 155 | logger.log(GLogLevelFlags.LEVEL_TO_LOGGER[level], 156 | f'{_to_string(domain)}: {_to_string(message)}') 157 | 158 | # keep a ref to the cb to stop it being GCd 159 | _log_handler_cb = ffi.callback('GLogFunc', _log_handler_callback) 160 | 161 | _log_handler_id = glib_lib.g_log_set_handler(_to_bytes('VIPS'), 162 | GLogLevelFlags.LEVEL_DEBUG | 163 | GLogLevelFlags.LEVEL_INFO | 164 | GLogLevelFlags.LEVEL_MESSAGE | 165 | GLogLevelFlags.LEVEL_WARNING | 166 | GLogLevelFlags.LEVEL_CRITICAL | 167 | GLogLevelFlags.LEVEL_ERROR | 168 | GLogLevelFlags.FLAG_FATAL | 169 | GLogLevelFlags.FLAG_RECURSION, 170 | _log_handler_cb, ffi.NULL) 171 | 172 | # ffi doesn't like us looking up methods during shutdown: make a note of the 173 | # remove handler here 174 | _remove_handler = glib_lib.g_log_remove_handler 175 | 176 | # we must remove the handler on exit or libvips may try to run the callback 177 | # during shutdown 178 | def _remove_log_handler(): 179 | global _log_handler_id 180 | global _remove_handler 181 | 182 | if _log_handler_id: 183 | _remove_handler(_to_bytes('VIPS'), _log_handler_id) 184 | _log_handler_id = None 185 | 186 | 187 | atexit.register(_remove_log_handler) 188 | 189 | from .enums import * 190 | from .base import * 191 | from .gobject import * 192 | from .gvalue import * 193 | from .vobject import * 194 | from .vinterpolate import * 195 | from .vconnection import * 196 | from .vsource import * 197 | from .vsourcecustom import * 198 | from .vtarget import * 199 | from .vtargetcustom import * 200 | from .voperation import * 201 | from .vimage import * 202 | from .vregion import * 203 | 204 | __all__ = ['API_mode'] 205 | -------------------------------------------------------------------------------- /pyvips/gobject.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import pyvips 4 | from pyvips import ffi, gobject_lib, _to_bytes, Error, type_name, \ 5 | at_least_libvips 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | # the python marshalers for gobject signal handling 10 | # - we keep a ref to each callback to stop them being GCd 11 | # - I tried to make this less copy-paste, but failed -- check again 12 | 13 | if pyvips.API_mode: 14 | @ffi.def_extern() 15 | def _marshal_image_progress(vi, pointer, handle): 16 | gobject_lib.g_object_ref(vi) 17 | image = pyvips.Image(vi) 18 | callback = ffi.from_handle(handle) 19 | progress = ffi.cast('VipsProgress*', pointer) 20 | callback(image, progress) 21 | _marshal_image_progress_cb = \ 22 | ffi.cast('GCallback', gobject_lib._marshal_image_progress) 23 | else: 24 | @ffi.callback('void(VipsImage*, void*, void*)') 25 | def _marshal_image_progress(vi, pointer, handle): 26 | gobject_lib.g_object_ref(vi) 27 | image = pyvips.Image(vi) 28 | callback = ffi.from_handle(handle) 29 | progress = ffi.cast('VipsProgress*', pointer) 30 | callback(image, progress) 31 | _marshal_image_progress_cb = \ 32 | ffi.cast('GCallback', _marshal_image_progress) 33 | 34 | _marshalers = { 35 | 'preeval': _marshal_image_progress_cb, 36 | 'eval': _marshal_image_progress_cb, 37 | 'posteval': _marshal_image_progress_cb, 38 | } 39 | 40 | if at_least_libvips(8, 9): 41 | if pyvips.API_mode: 42 | @ffi.def_extern() 43 | def _marshal_read(gobject, pointer, length, handle): 44 | buf = ffi.buffer(pointer, length) 45 | callback = ffi.from_handle(handle) 46 | return callback(buf) 47 | _marshal_read_cb = ffi.cast('GCallback', gobject_lib._marshal_read) 48 | else: 49 | @ffi.callback('gint64(VipsSourceCustom*, void*, gint64, void*)') 50 | def _marshal_read(gobject, pointer, length, handle): 51 | buf = ffi.buffer(pointer, length) 52 | callback = ffi.from_handle(handle) 53 | return callback(buf) 54 | _marshal_read_cb = ffi.cast('GCallback', _marshal_read) 55 | _marshalers['read'] = _marshal_read_cb 56 | 57 | if pyvips.API_mode: 58 | @ffi.def_extern() 59 | def _marshal_seek(gobject, offset, whence, handle): 60 | callback = ffi.from_handle(handle) 61 | return callback(offset, whence) 62 | _marshal_seek_cb = \ 63 | ffi.cast('GCallback', gobject_lib._marshal_seek) 64 | else: 65 | @ffi.callback('gint64(VipsSourceCustom*, gint64, int, void*)') 66 | def _marshal_seek(gobject, offset, whence, handle): 67 | callback = ffi.from_handle(handle) 68 | return callback(offset, whence) 69 | _marshal_seek_cb = ffi.cast('GCallback', _marshal_seek) 70 | _marshalers['seek'] = _marshal_seek_cb 71 | 72 | if pyvips.API_mode: 73 | @ffi.def_extern() 74 | def _marshal_write(gobject, pointer, length, handle): 75 | buf = ffi.buffer(pointer, length) 76 | callback = ffi.from_handle(handle) 77 | return callback(buf) 78 | _marshal_write_cb = ffi.cast('GCallback', gobject_lib._marshal_write) 79 | else: 80 | @ffi.callback('gint64(VipsTargetCustom*, void*, gint64, void*)') 81 | def _marshal_write(gobject, pointer, length, handle): 82 | buf = ffi.buffer(pointer, length) 83 | callback = ffi.from_handle(handle) 84 | return callback(buf) 85 | _marshal_write_cb = ffi.cast('GCallback', _marshal_write) 86 | _marshalers['write'] = _marshal_write_cb 87 | 88 | if pyvips.API_mode: 89 | @ffi.def_extern() 90 | def _marshal_finish(gobject, handle): 91 | callback = ffi.from_handle(handle) 92 | callback() 93 | _marshal_finish_cb = ffi.cast('GCallback', gobject_lib._marshal_finish) 94 | else: 95 | @ffi.callback('void(VipsTargetCustom*, void*)') 96 | def _marshal_finish(gobject, handle): 97 | callback = ffi.from_handle(handle) 98 | callback() 99 | _marshal_finish_cb = ffi.cast('GCallback', _marshal_finish) 100 | _marshalers['finish'] = _marshal_finish_cb 101 | 102 | if at_least_libvips(8, 13): 103 | if pyvips.API_mode: 104 | @ffi.def_extern() 105 | def _marshal_end(gobject, handle): 106 | callback = ffi.from_handle(handle) 107 | return callback() 108 | _marshal_end_cb = ffi.cast('GCallback', gobject_lib._marshal_end) 109 | else: 110 | @ffi.callback('int(VipsTargetCustom*, void*)') 111 | def _marshal_end(gobject, handle): 112 | callback = ffi.from_handle(handle) 113 | return callback() 114 | _marshal_end_cb = ffi.cast('GCallback', _marshal_end) 115 | _marshalers['end'] = _marshal_end_cb 116 | 117 | 118 | class GObject(object): 119 | """Manage GObject lifetime. 120 | 121 | """ 122 | __slots__ = ('_handles', 'pointer') 123 | 124 | def __init__(self, pointer): 125 | """Wrap around a pointer. 126 | 127 | Wraps a GObject instance around an underlying pointer. When the 128 | instance is garbage-collected, the underlying object is unreferenced. 129 | 130 | """ 131 | 132 | # we have to record all of the ffi.new_handle we make for callbacks on 133 | # this object to prevent them being GC'd 134 | self._handles = [] 135 | 136 | # record the pointer we were given to manage 137 | # on GC, unref 138 | self.pointer = ffi.gc(pointer, gobject_lib.g_object_unref) 139 | # logger.debug('GObject.__init__: pointer = %s', str(self.pointer)) 140 | 141 | @staticmethod 142 | def new_pointer_from_gtype(gtype): 143 | """Make a new GObject pointer from a gtype. 144 | 145 | This is useful for subclasses which need to control the construction 146 | process. 147 | 148 | You can pass the result pointer to the Python constructor for the 149 | object you are building. You will need to call VipsObject.build() to 150 | finish construction. 151 | 152 | Returns: 153 | A pointer to a new GObject. 154 | 155 | Raises: 156 | :class:`.Error` 157 | 158 | """ 159 | 160 | pointer = gobject_lib.g_object_new(gtype, ffi.NULL) 161 | if pointer == ffi.NULL: 162 | raise Error(f"can't create {type_name(gtype)}") 163 | 164 | return pointer 165 | 166 | def signal_connect(self, name, callback): 167 | """Connect to a signal on this object. 168 | 169 | The callback will be triggered every time this signal is issued on this 170 | instance. It will be passed the image ('self' here), and a single 171 | `void *` pointer from libvips. 172 | 173 | The value of the pointer, if any, depends on the signal -- for 174 | example, ::eval passes a pointer to a `VipsProgress` struct. 175 | 176 | """ 177 | 178 | if name not in _marshalers: 179 | raise Error(f'unsupported signal "{name}"') 180 | 181 | go = ffi.cast('GObject *', self.pointer) 182 | handle = ffi.new_handle(callback) 183 | # we need to keep refs to the ffi handle and the callback to prevent 184 | # them being GCed 185 | # the callback might be a bound method (a closure) rather than a simple 186 | # function, so it can vanish 187 | self._handles.append(handle) 188 | self._handles.append(callback) 189 | 190 | gobject_lib.g_signal_connect_data(go, _to_bytes(name), 191 | _marshalers[name], 192 | handle, ffi.NULL, 0) 193 | 194 | 195 | __all__ = ['GObject'] 196 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | README 2 | ====== 3 | 4 | .. image:: https://github.com/libvips/pyvips/workflows/CI/badge.svg 5 | :alt: Build Status 6 | :target: https://github.com/libvips/pyvips/actions 7 | 8 | PyPI package: 9 | 10 | https://pypi.python.org/pypi/pyvips 11 | 12 | conda package: 13 | 14 | https://anaconda.org/conda-forge/pyvips 15 | 16 | We have formatted docs online here: 17 | 18 | https://libvips.github.io/pyvips/ 19 | 20 | This module wraps the libvips image processing library: 21 | 22 | https://www.libvips.org/ 23 | 24 | The libvips docs are also very useful: 25 | 26 | https://www.libvips.org/API/current/ 27 | 28 | If you have the development headers for libvips installed and have a working C 29 | compiler, this module will use cffi API mode to try to build a libvips 30 | binary extension for your Python. 31 | 32 | If it is unable to build a binary extension, it will use cffi ABI mode 33 | instead and only needs the libvips shared library. This takes longer to 34 | start up and is typically ~20% slower in execution. You can find out if 35 | API mode is being used with: 36 | 37 | .. code-block:: python 38 | 39 | import pyvips 40 | 41 | print(pyvips.API_mode) 42 | 43 | This binding passes the vips test suite cleanly and with no leaks under 44 | python3 and pypy3 on Windows, macOS and Linux. 45 | 46 | How it works 47 | ------------ 48 | 49 | Programs that use ``pyvips`` don't manipulate images directly, instead 50 | they create pipelines of image processing operations building on a source 51 | image. When the end of the pipe is connected to a destination, the whole 52 | pipeline executes at once, streaming the image in parallel from source to 53 | destination a section at a time. 54 | 55 | Because ``pyvips`` is parallel, it's quick, and because it doesn't need to 56 | keep entire images in memory, it's light. For example, the libvips 57 | speed and memory use benchmark: 58 | 59 | https://github.com/libvips/libvips/wiki/Speed-and-memory-use 60 | 61 | Loads a large tiff image, shrinks by 10%, sharpens, and saves again. On this 62 | test ``pyvips`` is typically 3x faster than ImageMagick and needs 5x less 63 | memory. 64 | 65 | There's a handy chapter in the docs explaining how libvips opens files, 66 | which gives some more background. 67 | 68 | https://www.libvips.org/API/current/How-it-opens-files.html 69 | 70 | Binary installation 71 | ------------------- 72 | 73 | The quickest way to start with pyvips is by installing the binary package 74 | with: 75 | 76 | .. code-block:: shell 77 | 78 | $ pip install "pyvips[binary]" 79 | 80 | This installs a self-contained package with the most commonly needed 81 | libraries. It should just work on most platforms, including Linux, 82 | Windows and macOS, with 64 and 32 bit x64 and ARM CPUs. Note that this libvips 83 | is missing features like PDF load and OpenSlide support. 84 | 85 | If your platform is unsupported or the pre-built binary is unsuitable, you 86 | can install libvips separately instead. 87 | 88 | Local installation 89 | ------------------ 90 | 91 | You need the libvips shared library on your library search path, version 8.2 92 | or later, though at least version 8.9 is required for all features to work. 93 | See: 94 | 95 | https://www.libvips.org/install.html 96 | 97 | Linux 98 | ^^^^^ 99 | 100 | Perhaps: 101 | 102 | .. code-block:: shell 103 | 104 | $ sudo apt install libvips-dev --no-install-recommends 105 | $ pip install pyvips 106 | 107 | With python 3.11 and later, you will need to create a venv first and add 108 | `path/to/venv` to your `PATH`. Something like: 109 | 110 | .. code-block:: shell 111 | 112 | $ python3 -m venv ~/.local 113 | $ pip install pyvips 114 | 115 | macOS 116 | ^^^^^ 117 | 118 | With Homebrew: 119 | 120 | .. code-block:: shell 121 | 122 | $ brew install vips python pkg-config 123 | $ pip install pyvips 124 | 125 | Windows 126 | ^^^^^^^ 127 | 128 | On Windows, you can download a pre-compiled binary from the libvips website. 129 | 130 | https://www.libvips.org/install.html 131 | 132 | You'll need a 64-bit Python. The official one works well. 133 | 134 | You can add ``vips-dev-x.y\bin`` to your ``PATH``, but this will add a lot of 135 | extra DLLs to your search path and they might conflict with other programs, 136 | so it's usually safer to set ``PATH`` in your program. 137 | 138 | To set ``PATH`` from within Python, you need something like this at the 139 | start of your program: 140 | 141 | .. code-block:: python 142 | 143 | import os 144 | vipsbin = r'c:\vips-dev-8.16\bin' 145 | os.environ['PATH'] = vipsbin + ';' + os.environ['PATH'] 146 | 147 | For Python 3.8 and later, you need: 148 | 149 | .. code-block:: python 150 | 151 | import os 152 | vipsbin = r'c:\vips-dev-8.16\bin' 153 | add_dll_dir = getattr(os, 'add_dll_directory', None) 154 | if callable(add_dll_dir): 155 | add_dll_dir(vipsbin) 156 | else: 157 | os.environ['PATH'] = os.pathsep.join((vipsbin, os.environ['PATH'])) 158 | 159 | Now when you import pyvips, it should be able to find the DLLs. 160 | 161 | Conda 162 | ^^^^^ 163 | 164 | The Conda package includes a matching libvips binary, so just enter: 165 | 166 | .. code-block:: shell 167 | 168 | $ conda install --channel conda-forge pyvips 169 | 170 | Example 171 | ------- 172 | 173 | This sample program loads a JPG image, doubles the value of every green pixel, 174 | sharpens, and then writes the image back to the filesystem again: 175 | 176 | .. code-block:: python 177 | 178 | import pyvips 179 | 180 | image = pyvips.Image.new_from_file('some-image.jpg', access='sequential') 181 | image *= [1, 2, 1] 182 | mask = pyvips.Image.new_from_array([ 183 | [-1, -1, -1], 184 | [-1, 16, -1], 185 | [-1, -1, -1], 186 | ], scale=8) 187 | image = image.conv(mask, precision='integer') 188 | image.write_to_file('x.jpg') 189 | 190 | 191 | Notes 192 | ----- 193 | 194 | Local user install: 195 | 196 | .. code-block:: shell 197 | 198 | $ pip install -e .[binary] 199 | 200 | Run all tests: 201 | 202 | .. code-block:: shell 203 | 204 | $ tox 205 | 206 | Run test suite: 207 | 208 | .. code-block:: shell 209 | 210 | $ pytest 211 | 212 | Run a specific test: 213 | 214 | .. code-block:: shell 215 | 216 | $ pytest tests/test_saveload.py 217 | 218 | Run perf tests: 219 | 220 | .. code-block:: shell 221 | 222 | $ cd tests/perf 223 | $ ./run.sh 224 | 225 | Stylecheck: 226 | 227 | .. code-block:: shell 228 | 229 | $ flake8 230 | 231 | Generate HTML docs in ``doc/build/html``: 232 | 233 | .. code-block:: shell 234 | 235 | $ cd doc; sphinx-build -bhtml . build/html 236 | 237 | Regenerate enums: 238 | 239 | Make sure you have installed a libvips with all optional packages enabled, 240 | then 241 | 242 | .. code-block:: shell 243 | 244 | $ cd examples; \ 245 | ./gen-enums.py ~/GIT/libvips/build/libvips/Vips-8.0.gir > enums.py 246 | 247 | Then check and move `enums.py` into `pyvips/`. 248 | 249 | Regenerate autodocs: 250 | 251 | Make sure you have installed a libvips with all optional packages enabled, 252 | then 253 | 254 | .. code-block:: shell 255 | 256 | $ cd doc; \ 257 | python3 -c "import pyvips; pyvips.Operation.generate_sphinx_all()" > x 258 | 259 | And copy-paste ``x`` into the obvious place in ``doc/vimage.rst``. 260 | 261 | Update version number: 262 | 263 | .. code-block:: shell 264 | 265 | $ vi pyvips/version.py 266 | $ vi doc/conf.py 267 | 268 | Update pypi package: 269 | 270 | .. code-block:: shell 271 | 272 | $ python3 -m build --sdist 273 | 274 | Ignore the deprecation warnings about the license, we need to update the 275 | build backend before we can fix this. 276 | 277 | .. code-block:: shell 278 | 279 | $ twine upload --repository pyvips dist/* 280 | $ git tag -a v3.0.0 -m "as uploaded to pypi" 281 | $ git push origin v3.0.0 282 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ## Version 3.1.1 (released 9 December 2025) 2 | 3 | - fix get_gainmap arguments [jcupitt] 4 | - fix __array__ copy argument test [jcupitt] 5 | - add a test [jcupitt] 6 | 7 | ## Version 3.1.0 (released 8 December 2025) 8 | 9 | - fix glib DLL name with Conan [boussaffawalid] 10 | - update docs for libvips 8.18 [jcupitt] 11 | - add `get_gainmap()` [jcupitt] 12 | - remove redundant `revalidate` flags from docs [kleisauke] 13 | 14 | ## Version 3.0.0 (released 28 April 2025) 15 | 16 | - drop support for Python 2.7, require Python >= 3.7 [kleisauke] 17 | - migrate to `pyproject.toml` [kleisauke] 18 | - fix a small memleak in `write_to_buffer()` [kleisauke] 19 | - add `[binary]` install option [kleisauke] 20 | - add `shutdown()` [jcupitt] 21 | 22 | ## Version 2.2.3 (released 28 April 2024) 23 | 24 | - ensure compatibility with a single shared libvips library [kleisauke] 25 | - add flags_dict(), enum_dict() for better flags introspection 26 | - improve generation of `enums.py` 27 | - add `stream.py` example 28 | - fix a missing reference issue with custom sources 29 | - fix `addalpha` with scrgb images [RiskoZoSlovenska] 30 | - fix macos compat, again [kleisauke] 31 | 32 | ## Version 2.2.2 (released 4 Jan 2023) 33 | 34 | * fix with libvips nodeprecated [kleisauke] 35 | * update README notes for py3.8+ on win [CristiFati] 36 | * fix VipsObect.print_all() [jcupitt] 37 | * add split, join, annotate animation examples [jcupitt] 38 | * work around broken ffi bool type [amtsak] 39 | * flag classes are now integer-valued so you can OR them [jcupitt] 40 | * update docs for 8.15 [jcupitt] 41 | 42 | ## Version 2.2.1 (released 12 Jun 2022) 43 | 44 | * add seek and end handlers for TargetCustom [jcupitt] 45 | * add `block_untrusted_set`, `operation_block_set` [jcupitt] 46 | * update for libvips 8.13 [jcupitt] 47 | 48 | ## Version 2.2.0 (released 18 Apr 2022) 49 | 50 | * `repr()` will print matrix images as matrices [jcupitt] 51 | * more robust bandwise index/slice; added fancy slicing (step != 1) [erdmann] 52 | * fix `im.bandjoin([])`, now returns `im` [erdmann] 53 | * add numpy-style extended indexing (index with list of ints or bools) [erdmann] 54 | * earlier detection of unknown methods and class methods [jcupitt] 55 | * add conversion from Image to numpy array via 'Image.__array__` [erdmann] 56 | * add `Image.fromarray()` for conversion from numpy-ish arrays [erdmann] 57 | * add `invalidate()` [jcupitt] 58 | * add array-like functionality to `Image.new_from_array()` for conversion from 59 | numpy-ish arrays [erdmann] 60 | * add `Image.numpy()` (convenient for method chaining) [erdmann] 61 | * add `tolist()` [erdmann] 62 | * accept `pathlib.Path` objects for filenames (py3 only) [erdmann] 63 | * cache pspec lookups for a 10% speed boost [jcupitt] 64 | 65 | ## Version 2.1.16 (started 28 Jun 2021) 66 | 67 | * fix logging of deprecated args [manthey] 68 | * add shepards example [tourtiere] 69 | * update docs for 8.12 [jcupitt] 70 | * add pagesplit(), pagejoin(), get_page_height(), get_n_pages() [jcupitt] 71 | * add atan2() [jcupitt] 72 | * add `cache_get_max()`, `cache_get_max_mem()`, `cache_get_max_files()`, 73 | `cache_get_size()` [hroskes] 74 | * don't generate docs for deprecated arguments [jcupitt] 75 | * buffer save tries with the target API first [jcupitt] 76 | * add hyperbolic functions `sinh`, `cosh`, `tanh`, `asinh`, `acosh`, 77 | `atanh` [jcupitt] 78 | * add `values_for_flag` [kleisauke] 79 | 80 | ## Version 2.1.15 (27 Dec 2020) 81 | 82 | * better autodocs for enums [int-ua] 83 | * better unreffing if operators fail [kleisauke] 84 | 85 | ## Version 2.1.14 (18 Dec 2020) 86 | 87 | * add `stdio.py` example 88 | * update examples 89 | * improve formatting of enum documentation 90 | * regenerate docs 91 | * remove old `vips_free` declaration, fixing API build on some platforms 92 | [rajasagashe] 93 | 94 | ## Version 2.1.13 (4 Jul 2020) 95 | 96 | * better diagnostics for API mode install failure [kleisauke] 97 | * revise docs [int-ua] 98 | * better reference tracking for new_from_memory [aspadm] 99 | 100 | ## Version 2.1.12 (17 Feb 2020) 101 | 102 | * update enums.py [tony612] 103 | * add gen-enums.py [jcupitt] 104 | * improve custom source/target types [jcupitt] 105 | * revise types for set_blob to fix exception with old libvips [jcupitt] 106 | * fix 32-bit support [dqxpb] 107 | * remove pytest-runner from pipy deps [lgbaldoni] 108 | * add watermark with image example [jcupitt] 109 | 110 | ## Version 2.1.11 (7 Nov 2019) 111 | 112 | * revise README [jcupitt] 113 | * add watermark example [jcupitt] 114 | * fix syntax highlighting in README [favorable-mutation] 115 | * add signal handling [jcupitt] 116 | * add Source / Target support [jcupitt] 117 | * add perf tests [kleisauke] 118 | * speed up Operation.call [kleisauke] 119 | * fix logging [h4l] 120 | 121 | ## Version 2.1.8 (1 Jul 2019) 122 | 123 | * fix regression with py27 [jcupitt] 124 | 125 | ## Version 2.1.7 (1 Jul 2019) 126 | 127 | * prevent exponential growth of reference tables in some cases [NextGuido] 128 | 129 | ## Version 2.1.6 (7 Jan 2019) 130 | 131 | * switch to new-style callbacks [kleisauke] 132 | * add get_suffixes() [jcupitt] 133 | * add Region [jcupitt] 134 | * better handling of NULL strings from ffi [jcupitt] 135 | * add support for dealing with uint64 types [kleisauke] 136 | 137 | ## Version 2.1.5 (18 Dec 2018) 138 | 139 | * better behaviour for new_from_memory fixes some segvs [wppd] 140 | * added addalpha/hasalpha [jcupitt] 141 | 142 | ## Version 2.1.4 (3 Oct 2018) 143 | 144 | * update links for repo move [jcupitt] 145 | * update autodocs for libvips 8.7 [jcupitt] 146 | 147 | ## Version 2.1.3 (3 March 2018) 148 | 149 | * record header version number in binary module and check compatibility with 150 | the library during startup [jcupitt] 151 | * add optional output params to docs [kleisauke] 152 | * update docs [jcupitt] 153 | * add some libvips 8.7 tests [jcupitt] 154 | * move to pytest [kleisauke] 155 | * better handling of many-byte values in py3 new_from_memory [MatthiasKohl] 156 | * better handling of utf-8 i18n text [felixbuenemann] 157 | * add enum introspection [kleisauke] 158 | * move the libvips test suite back to libvips, just test pyvips here [jcupitt] 159 | * fix five small memleaks [kleisauke] 160 | 161 | ## Version 2.1.2 (1 March 2018) 162 | 163 | * only use get_fields on libvips 8.5+ [rebkwok] 164 | * only use parent_instance on libvips 8.4+ [rebkwok] 165 | * relative import for decl 166 | 167 | ## Version 2.1.1 (25 February 2018) 168 | 169 | * switch to sdist 170 | * better ABI mode fallback behaviour 171 | 172 | ## Version 2.1.0 (17 November 2017) 173 | 174 | * support cffi API mode as well: much faster startup, about 20% faster on the 175 | test suite [jcupitt] 176 | * on install, it tries to build a binary interface, and if that fails, falls 177 | back to ABI mode [jcupitt] 178 | * better error for bad kwarg [geniass] 179 | 180 | ## Version 2.0.6 (22 February 2017) 181 | 182 | * add version numbers to library names on linux 183 | 184 | ## Version 2.0.5 (8 September 2017) 185 | 186 | * minor polish 187 | * more tests 188 | * add `composite` convenience method 189 | * move tests outside module [greut] 190 | * switch to tox [greut] 191 | * allow info message logging 192 | 193 | ## Version 2.0.4 (3 September 2017) 194 | 195 | * clear error log after failed get_typeof in get() workaround 196 | * more tests pass with older libvips 197 | * fix typo in logging handler 198 | 199 | ## Version 2.0.3 (2 September 2017) 200 | 201 | * fix get() with old libvips 202 | * better collapse for docs [kleisauke] 203 | * add `get_fields()` 204 | 205 | ## Version 2.0.2 (26 August 2017) 206 | 207 | * support `pyvips.__version__` 208 | * add `version()` to get libvips version number 209 | * add `cache_set_max()`, `cache_set_max_mem()`, `cache_set_max_files()`, 210 | `cache_set_trace()` 211 | * all glib log levels sent to py logger 212 | * docs are collapsed for less scrolling [kleisauke] 213 | 214 | ## Version 2.0.1 (23 August 2017) 215 | 216 | * doc revisions 217 | * fix test suite on Windows 218 | * redirect libvips warnings to logging 219 | * fix debug logging 220 | 221 | ## Version 2.0.0 (19 August 2017) 222 | 223 | * rewrite on top of 'cffi' 224 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # pyvips documentation build configuration file, created by 4 | # sphinx-quickstart on Wed Aug 9 15:19:17 2017. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | # If extensions (or modules to document with autodoc) are in another directory, 16 | # add these directories to sys.path here. If the directory is relative to the 17 | # documentation root, use os.path.abspath to make it absolute, like shown here. 18 | 19 | import os 20 | import sys 21 | import sphinx_rtd_theme 22 | sys.path.insert(0, os.path.abspath('..')) 23 | 24 | 25 | # -- General configuration ------------------------------------------------ 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | # 29 | # needs_sphinx = '1.0' 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = [ 35 | 'sphinx.ext.autodoc', 36 | 'sphinx.ext.autosummary', 37 | 'sphinx.ext.mathjax', 38 | 'sphinx.ext.viewcode', 39 | 'sphinx.ext.githubpages', 40 | 'sphinx.ext.napoleon', 41 | ] 42 | 43 | autodoc_member_order = 'bysource' 44 | 45 | # Add any paths that contain templates here, relative to this directory. 46 | templates_path = ['_templates'] 47 | 48 | # The suffix(es) of source filenames. 49 | # You can specify multiple suffix as a list of string: 50 | # 51 | # source_suffix = ['.rst', '.md'] 52 | source_suffix = '.rst' 53 | 54 | # The master toctree document. 55 | master_doc = 'index' 56 | 57 | # General information about the project. 58 | project = u'pyvips' 59 | copyright = u'2019, John Cupitt' 60 | author = u'John Cupitt' 61 | 62 | # The version info for the project you're documenting, acts as replacement for 63 | # |version| and |release|, also used in various other places throughout the 64 | # built documents. 65 | # 66 | # The short X.Y version. 67 | version = u'3.1' 68 | # The full version, including alpha/beta/rc tags. 69 | release = u'3.1.1' 70 | 71 | # The language for content autogenerated by Sphinx. Refer to documentation 72 | # for a list of supported languages. 73 | # 74 | # This is also used if you do content translation via gettext catalogs. 75 | # Usually you set "language" from the command line for these cases. 76 | language = None 77 | 78 | # List of patterns, relative to source directory, that match files and 79 | # directories to ignore when looking for source files. 80 | # This patterns also effect to html_static_path and html_extra_path 81 | exclude_patterns = ['global.rst'] 82 | 83 | # The name of the Pygments (syntax highlighting) style to use. 84 | pygments_style = 'sphinx' 85 | 86 | # If true, `todo` and `todoList` produce output, else they produce nothing. 87 | todo_include_todos = False 88 | 89 | 90 | # -- Options for HTML output ---------------------------------------------- 91 | 92 | # The theme to use for HTML and HTML Help pages. See the documentation for 93 | # a list of builtin themes. 94 | # 95 | html_theme = 'sphinx_rtd_theme' 96 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 97 | 98 | # Theme options are theme-specific and customize the look and feel of a theme 99 | # further. For a list of options available for each theme, see the 100 | # documentation. 101 | # 102 | # html_theme_options = {} 103 | 104 | # Add any paths that contain custom static files (such as style sheets) here, 105 | # relative to this directory. They are copied after the builtin static files, 106 | # so a file named "default.css" will overwrite the builtin "default.css". 107 | html_static_path = ['_static'] 108 | 109 | # Custom sidebar templates, must be a dictionary that maps document names 110 | # to template names. 111 | # 112 | # This is required for the alabaster theme 113 | # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars 114 | html_sidebars = { 115 | '**': [ 116 | 'about.html', 117 | 'navigation.html', 118 | 'searchbox.html', 119 | ] 120 | } 121 | 122 | html_context = { 123 | # Enable the "Edit on GitHub" link within the header of each page. 124 | 'display_github': True, 125 | # Set the following variables to generate the resulting github URL for 126 | # each page. 127 | 'github_user': 'libvips', 128 | 'github_repo': 'pyvips', 129 | 'github_version': 'master/doc/' 130 | } 131 | 132 | # -- Options for HTMLHelp output ------------------------------------------ 133 | 134 | # Output file base name for HTML help builder. 135 | htmlhelp_basename = 'pyvipsdoc' 136 | 137 | 138 | # -- Options for LaTeX output --------------------------------------------- 139 | 140 | latex_elements = { 141 | # The paper size ('letterpaper' or 'a4paper'). 142 | # 143 | # 'papersize': 'letterpaper', 144 | 145 | # The font size ('10pt', '11pt' or '12pt'). 146 | # 147 | # 'pointsize': '10pt', 148 | 149 | # Additional stuff for the LaTeX preamble. 150 | # 151 | # 'preamble': '', 152 | 153 | # Latex figure (float) alignment 154 | # 155 | # 'figure_align': 'htbp', 156 | } 157 | 158 | # Grouping the document tree into LaTeX files. List of tuples 159 | # (source start file, target name, title, 160 | # author, documentclass [howto, manual, or own class]). 161 | latex_documents = [ 162 | (master_doc, 'pyvips.tex', u'pyvips Documentation', 163 | u'john', 'manual'), 164 | ] 165 | 166 | 167 | # -- Options for manual page output --------------------------------------- 168 | 169 | # One entry per manual page. List of tuples 170 | # (source start file, name, description, authors, manual section). 171 | man_pages = [ 172 | (master_doc, 'pyvips', u'pyvips Documentation', 173 | [author], 1) 174 | ] 175 | 176 | 177 | # -- Options for Texinfo output ------------------------------------------- 178 | 179 | # Grouping the document tree into Texinfo files. List of tuples 180 | # (source start file, target name, title, author, 181 | # dir menu entry, description, category) 182 | texinfo_documents = [ 183 | (master_doc, 'pyvips', u'pyvips Documentation', 184 | author, 'pyvips', 'One line description of project.', 185 | 'Miscellaneous'), 186 | ] 187 | 188 | 189 | # see https://stackoverflow.com/questions/20569011 190 | # adds autoautosummary directive, see vimage.rst 191 | 192 | # try to exclude deprecated 193 | def skip_deprecated(app, what, name, obj, skip, options): 194 | if hasattr(obj, "func_dict") and "__deprecated__" in obj.func_dict: 195 | print("skipping " + name) 196 | return True 197 | return skip or False 198 | 199 | 200 | def setup(app): 201 | app.connect('autodoc-skip-member', skip_deprecated) 202 | try: 203 | from sphinx.ext.autosummary import Autosummary 204 | from sphinx.ext.autosummary import get_documenter 205 | from docutils.parsers.rst import directives 206 | from sphinx.util.inspect import safe_getattr 207 | 208 | class AutoAutoSummary(Autosummary): 209 | 210 | option_spec = { 211 | 'methods': directives.unchanged, 212 | 'attributes': directives.unchanged 213 | } 214 | 215 | required_arguments = 1 216 | 217 | @staticmethod 218 | def get_members(obj, typ, include_public=None): 219 | if not include_public: 220 | include_public = [] 221 | items = [] 222 | for name in dir(obj): 223 | try: 224 | documenter = get_documenter(safe_getattr(obj, name), 225 | obj) 226 | except AttributeError: 227 | continue 228 | if documenter.objtype == typ: 229 | items.append(name) 230 | public = [x for x in items 231 | if x in include_public or not x.startswith('_')] 232 | return public, items 233 | 234 | def run(self): 235 | clazz = str(self.arguments[0]) 236 | try: 237 | (module_name, class_name) = clazz.rsplit('.', 1) 238 | m = __import__(module_name, globals(), locals(), 239 | [class_name]) 240 | c = getattr(m, class_name) 241 | if 'methods' in self.options: 242 | _, methods = self.get_members(c, 243 | 'method', ['__init__']) 244 | 245 | self.content = ["~%s.%s" % (clazz, method) 246 | for method in methods 247 | if not method.startswith('_')] 248 | if 'attributes' in self.options: 249 | _, attribs = self.get_members(c, 'attribute') 250 | self.content = ["~%s.%s" % (clazz, attrib) 251 | for attrib in attribs 252 | if not attrib.startswith('_')] 253 | finally: 254 | return super(AutoAutoSummary, self).run() 255 | 256 | app.add_directive('autoautosummary', AutoAutoSummary) 257 | except BaseException as e: 258 | raise e 259 | -------------------------------------------------------------------------------- /tests/test_image.py: -------------------------------------------------------------------------------- 1 | # vim: set fileencoding=utf-8 : 2 | 3 | import pyvips 4 | import pytest 5 | from helpers import JPEG_FILE, UHDR_FILE, skip_if_no 6 | 7 | 8 | class TestImage: 9 | def test_pagejoin(self): 10 | image = pyvips.Image.new_from_file(JPEG_FILE) 11 | many_page = image.pagejoin([image, image]) 12 | assert many_page.get_page_height() == image.height 13 | assert many_page.height / many_page.get_page_height() == 3 14 | 15 | def test_pagesplit(self): 16 | image = pyvips.Image.new_from_file(JPEG_FILE) 17 | many_page = image.pagejoin([image, image]) 18 | image_list = many_page.pagesplit() 19 | assert len(image_list) == 3 20 | 21 | def test_bandjoin(self): 22 | black = pyvips.Image.black(16, 16) 23 | a = black.draw_rect(1, 0, 0, 1, 1) 24 | b = black.draw_rect(2, 0, 0, 1, 1) 25 | c = black.draw_rect(3, 0, 0, 1, 1) 26 | im = a.bandjoin([b, c, a, b, c, a, b, c]) 27 | 28 | assert im.width == 16 29 | assert im.height == 16 30 | assert im.bands == 9 31 | 32 | x = im.bandjoin([]) 33 | assert x.bands == 9 34 | 35 | def test_bandslice(self): 36 | black = pyvips.Image.black(16, 16) 37 | a = black.draw_rect(1, 0, 0, 1, 1) 38 | b = black.draw_rect(2, 0, 0, 1, 1) 39 | c = black.draw_rect(3, 0, 0, 1, 1) 40 | d = black.draw_rect(4, 0, 0, 1, 1) 41 | e = black.draw_rect(5, 0, 0, 1, 1) 42 | f = black.draw_rect(6, 0, 0, 1, 1) 43 | 44 | im = black.bandjoin([a, b, c, d, e, f]) 45 | 46 | seq = list(range(im.bands)) 47 | 48 | x = im[1] 49 | 50 | assert x.bands == 1 51 | assert x(0, 0) == [seq[1]] 52 | 53 | x = im[1:4] 54 | 55 | assert x.bands == 3 56 | assert x(0, 0) == seq[1:4] 57 | 58 | x = im[1:4][::-1] 59 | 60 | assert x.bands == 3 61 | assert x(0, 0) == seq[1:4][::-1] 62 | 63 | x = im[-7] 64 | assert x(0, 0) == [seq[-7]] 65 | 66 | x = im[::-1] 67 | 68 | assert x(0, 0) == seq[::-1] 69 | 70 | x = im[4:6] 71 | 72 | assert x(0, 0) == seq[4:6] 73 | 74 | x = im[5:3:-1] 75 | 76 | assert x(0, 0) == seq[5:3:-1] 77 | 78 | x = im[2:4:2] 79 | 80 | assert x(0, 0) == seq[2:4:2] 81 | 82 | c = im[2:0:-2] 83 | 84 | assert x(0, 0) == seq[2:0:-2] 85 | 86 | x = im[::-2] 87 | 88 | assert x(0, 0) == seq[::-2] 89 | 90 | x = im[1::-2] 91 | 92 | assert x(0, 0) == seq[1::-2] 93 | 94 | x = im[:5:-2] 95 | 96 | assert x(0, 0) == seq[:5:-2] 97 | 98 | x = im[5:0:-2] 99 | 100 | assert x(0, 0) == seq[5:0:-2] 101 | 102 | x = im[-10:10] 103 | 104 | assert x(0, 0) == seq[-10:10] 105 | 106 | indices = [1, 2, 5] 107 | x = im[indices] 108 | 109 | assert x(0, 0) == [seq[i] for i in indices] 110 | 111 | indices = [1, 2, -7] 112 | x = im[indices] 113 | 114 | assert x(0, 0) == [seq[i] for i in indices] 115 | 116 | indices = [1] 117 | x = im[indices] 118 | 119 | assert x(0, 0) == [seq[1]] 120 | 121 | indices = [-1] 122 | x = im[indices] 123 | 124 | assert x(0, 0) == [seq[-1]] 125 | 126 | boolslice = [True, True, False, False, True, True, False] 127 | x = im[boolslice] 128 | assert x(0, 0) == [seq[i] for i, b in enumerate(boolslice) if b] 129 | 130 | with pytest.raises(IndexError): 131 | x = im[4:1] 132 | 133 | with pytest.raises(IndexError): 134 | x = im[-(im.bands + 1)] 135 | 136 | with pytest.raises(IndexError): 137 | x = im[im.bands] 138 | 139 | with pytest.raises(IndexError): 140 | empty = [False] * im.bands 141 | x = im[empty] 142 | 143 | with pytest.raises(IndexError): 144 | notenough = [True] * (im.bands - 1) 145 | x = im[notenough] 146 | 147 | with pytest.raises(IndexError): 148 | toomany = [True] * (im.bands + 1) 149 | x = im[toomany] 150 | 151 | with pytest.raises(IndexError): 152 | empty = [] 153 | x = im[empty] 154 | 155 | with pytest.raises(IndexError): 156 | oob = [2, 3, -8] 157 | x = im[oob] 158 | 159 | with pytest.raises(IndexError): 160 | mixed = [True, 1, True, 2, True, 3, True] 161 | x = im[mixed] 162 | 163 | with pytest.raises(IndexError): 164 | wrongtypelist = ['a', 'b', 'c'] 165 | x = im[wrongtypelist] 166 | 167 | with pytest.raises(IndexError): 168 | wrongargtype = dict(a=1, b=2) 169 | x = im[wrongargtype] 170 | 171 | def test_invalidate(self): 172 | try: 173 | import numpy as np 174 | except ImportError: 175 | pytest.skip('numpy not available') 176 | 177 | a = np.zeros((1, 1)) 178 | p = pyvips.Image.new_from_memory(a.data, 1, 1, 1, 'double') 179 | v = p(0, 0) 180 | assert v == [0] 181 | a[0, 0] = 1 182 | v = p(0, 0) 183 | assert v == [0] 184 | p.invalidate() 185 | v = p(0, 0) 186 | assert v == [1] 187 | 188 | def test_to_numpy(self): 189 | try: 190 | import numpy as np 191 | except ImportError: 192 | pytest.skip('numpy not available') 193 | 194 | xy = pyvips.Image.xyz(4, 5) 195 | 196 | # using __array__ interface: 197 | 198 | from pyvips.vimage import TYPESTR_TO_FORMAT 199 | for typestr, format in TYPESTR_TO_FORMAT.items(): 200 | this_xy = xy.cast(format) 201 | if typestr == '|b1': 202 | yx = np.asarray(this_xy, dtype=typestr) 203 | else: 204 | yx = np.array(this_xy) 205 | 206 | assert yx.dtype == np.dtype(typestr) 207 | assert yx.shape == (5, 4, 2) 208 | assert all(yx.max(axis=(0, 1)) == np.array([3, 4], dtype=typestr)) 209 | 210 | x, y = xy 211 | x_iy = pyvips.Image.complexform(x, y) 212 | x_iy_dp = x_iy.cast('dpcomplex') 213 | 214 | a = np.array(x_iy) 215 | assert a.shape == (5, 4) 216 | assert a.dtype == np.dtype('complex64') 217 | assert a[4, 3].item() == 3+4j 218 | 219 | a = np.array(x_iy_dp) 220 | assert a.shape == (5, 4) 221 | assert a.dtype == np.dtype('complex128') 222 | assert a[4, 3].item() == 3+4j 223 | 224 | xyxyxy = xy.bandjoin([xy, xy]) 225 | a = np.array(xyxyxy) 226 | assert a.shape == (5, 4, 6) 227 | 228 | # axes collapsing: 229 | xyxyxy_col = xyxyxy.crop(3, 0, 1, 5) 230 | assert xyxyxy_col.width == 1 231 | assert xyxyxy_col.height == 5 232 | a = np.array(xyxyxy_col) 233 | assert a.shape == (5, 1, 6) 234 | 235 | xyxyxy_row = xyxyxy.crop(0, 4, 4, 1) 236 | assert xyxyxy_row.width == 4 237 | assert xyxyxy_row.height == 1 238 | a = np.array(xyxyxy_row) 239 | assert a.shape == (1, 4, 6) 240 | 241 | xyxyxy_px = xyxyxy.crop(3, 4, 1, 1) 242 | assert xyxyxy_px.width == 1 243 | assert xyxyxy_px.height == 1 244 | a = np.array(xyxyxy_px) 245 | assert a.shape == (1, 1, 6) 246 | 247 | a = np.array(xyxyxy_px[0]) 248 | assert a.ndim == 0 249 | 250 | # automatic conversion to array on ufunc application: 251 | 252 | d = np.diff(xy.cast('short'), axis=1) 253 | assert (d[..., 0] == 1).all() 254 | assert (d[..., 1] == 0).all() 255 | 256 | # tests of .numpy() method: 257 | 258 | a = pyvips.Image.xyz(256, 1)[0].numpy().squeeze() 259 | 260 | assert all(a == np.arange(256)) 261 | 262 | def test_scipy(self): 263 | try: 264 | import numpy as np 265 | except ImportError: 266 | pytest.skip('numpy not available') 267 | 268 | try: 269 | from scipy import ndimage 270 | except ImportError: 271 | pytest.skip('scipy not available') 272 | 273 | black = pyvips.Image.black(16, 16) 274 | a = black.draw_rect(1, 0, 0, 1, 1) 275 | 276 | d = ndimage.distance_transform_edt(a == 0) 277 | assert np.allclose(d[-1, -1], (2 * 15 ** 2) ** 0.5) 278 | 279 | def test_torch(self): 280 | try: 281 | import numpy as np 282 | except ImportError: 283 | pytest.skip('numpy not available') 284 | 285 | try: 286 | import torch 287 | except ImportError: 288 | pytest.skip('torch not available') 289 | 290 | # torch to Image: 291 | x = torch.outer(torch.arange(10), torch.arange(5)) 292 | with pytest.raises(ValueError): 293 | # no vips format for int64 294 | im = pyvips.Image.new_from_array(x) 295 | 296 | x = x.float() 297 | 298 | im = pyvips.Image.new_from_array(x) 299 | assert im.width == 5 300 | assert im.height == 10 301 | assert im(4, 9) == [36] 302 | assert im.format == 'float' 303 | 304 | # Image to torch: 305 | im = pyvips.Image.zone(5, 5) 306 | t = torch.from_numpy(np.asarray(im)) 307 | assert t[2, 2] == 1. 308 | 309 | def test_from_numpy(self): 310 | try: 311 | import numpy as np 312 | except ImportError: 313 | pytest.skip('numpy not available') 314 | 315 | a = np.indices((5, 4)).transpose(1, 2, 0) # (5, 4, 2) 316 | 317 | with pytest.raises(ValueError): 318 | # no way in for int64 319 | yx = pyvips.Image.new_from_array(a) 320 | 321 | from pyvips.vimage import TYPESTR_TO_FORMAT 322 | 323 | for typestr, format in TYPESTR_TO_FORMAT.items(): 324 | if typestr == '|b1': 325 | continue 326 | a = np.indices((5, 4), dtype=typestr).transpose(1, 2, 0) 327 | yx = pyvips.Image.new_from_array(a) 328 | assert yx.format == format 329 | 330 | a = np.zeros((2, 2, 2, 2)) 331 | 332 | with pytest.raises(ValueError): 333 | # too many dimensions 334 | im = pyvips.Image.new_from_array(a) 335 | 336 | a = np.ones((2, 2, 2), dtype=bool) 337 | im = pyvips.Image.new_from_array(a) 338 | assert im.max() == 255 339 | 340 | a = np.ones((2, 2, 2), dtype='S8') 341 | with pytest.raises(ValueError): 342 | # no way in for strings 343 | im = pyvips.Image.new_from_array(a) 344 | 345 | # test handling of strided data 346 | a = np.ones((1000, 1000, 3), dtype='uint8')[::10, ::10] 347 | im = pyvips.Image.new_from_array(a) 348 | assert im.width == 100 349 | assert im.height == 100 350 | assert im.bands == 3 351 | 352 | class FakeArray(object): 353 | @property 354 | def __array_interface__(self): 355 | return {'shape': (1, 1, 1), 356 | 'typestr': '|u1', 357 | 'version': 3} 358 | 359 | with pytest.raises(TypeError): 360 | # Handle evil objects that don't behave like ndarrays 361 | im = pyvips.Image.new_from_array(FakeArray()) 362 | 363 | def test_tolist(self): 364 | im = pyvips.Image.complexform(*pyvips.Image.xyz(3, 4)) 365 | 366 | assert im.tolist()[-1][-1] == 2+3j 367 | 368 | im = im.cast('dpcomplex') 369 | 370 | assert im.tolist()[-1][-1] == 2+3j 371 | 372 | lst = [[1, 2, 3], [4, 5, 6]] 373 | 374 | im = pyvips.Image.new_from_array(lst) 375 | 376 | assert im.tolist() == lst 377 | 378 | assert im.cast('float').tolist() == lst 379 | assert im.cast('complex').tolist() == lst 380 | 381 | def test_from_PIL(self): 382 | try: 383 | import PIL.Image 384 | except ImportError: 385 | pytest.skip('PIL not available') 386 | 387 | pim = PIL.Image.new('RGB', (42, 23)) 388 | 389 | im = pyvips.Image.new_from_array(pim) 390 | assert im.format == 'uchar' 391 | assert im.interpretation == 'srgb' 392 | assert im.width == pim.width 393 | assert im.height == pim.height 394 | assert im.min() == 0 395 | assert im.max() == 0 396 | 397 | @skip_if_no('uhdrload') 398 | def test_gainmap(self): 399 | def crop_gainmap(image): 400 | gainmap = image.get_gainmap() 401 | 402 | if gainmap: 403 | new_gainmap = gainmap.crop(0, 0, 10, 10) 404 | image = image.copy() 405 | image.set_type(pyvips.GValue.image_type, 406 | "gainmap", new_gainmap) 407 | 408 | return image 409 | 410 | image = pyvips.Image.new_from_file(UHDR_FILE) 411 | image = crop_gainmap(image) 412 | buf = image.write_to_buffer(".jpg") 413 | new_image = pyvips.Image.new_from_buffer(buf, "") 414 | new_gainmap = new_image.get_gainmap() 415 | 416 | assert new_gainmap.width == 10 417 | -------------------------------------------------------------------------------- /pyvips/gvalue.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import numbers 3 | 4 | import pyvips 5 | from pyvips import ffi, vips_lib, gobject_lib, \ 6 | glib_lib, Error, _to_bytes, _to_string, type_name, type_from_name, \ 7 | at_least_libvips 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | class GValue(object): 13 | 14 | """Wrap GValue in a Python class. 15 | 16 | This class wraps :class:`.GValue` in a convenient interface. You can use 17 | instances of this class to get and set :class:`.GObject` properties. 18 | 19 | On construction, :class:`.GValue` is all zero (empty). You can pass it to 20 | a get function to have it filled by :class:`.GObject`, or use init to 21 | set a type, set to set a value, then use it to set an object property. 22 | 23 | GValue lifetime is managed automatically. 24 | 25 | """ 26 | __slots__ = ('pointer', 'gvalue') 27 | 28 | # look up some common gtypes at init for speed 29 | gbool_type = type_from_name('gboolean') 30 | gint_type = type_from_name('gint') 31 | guint64_type = type_from_name('guint64') 32 | gdouble_type = type_from_name('gdouble') 33 | gstr_type = type_from_name('gchararray') 34 | genum_type = type_from_name('GEnum') 35 | gflags_type = type_from_name('GFlags') 36 | gobject_type = type_from_name('GObject') 37 | image_type = type_from_name('VipsImage') 38 | array_int_type = type_from_name('VipsArrayInt') 39 | array_double_type = type_from_name('VipsArrayDouble') 40 | array_image_type = type_from_name('VipsArrayImage') 41 | refstr_type = type_from_name('VipsRefString') 42 | blob_type = type_from_name('VipsBlob') 43 | source_type = type_from_name('VipsSource') 44 | target_type = type_from_name('VipsTarget') 45 | 46 | pyvips.vips_lib.vips_band_format_get_type() 47 | format_type = type_from_name('VipsBandFormat') 48 | 49 | if at_least_libvips(8, 6): 50 | pyvips.vips_lib.vips_blend_mode_get_type() 51 | blend_mode_type = type_from_name('VipsBlendMode') 52 | 53 | # map a gtype to the name of the corresponding Python type 54 | _gtype_to_python = { 55 | gbool_type: 'bool', 56 | gint_type: 'int', 57 | guint64_type: 'long', # Note: int and long are unified in Python 3 58 | gdouble_type: 'float', 59 | gstr_type: 'str', 60 | refstr_type: 'str', 61 | genum_type: 'str', 62 | gflags_type: 'int', 63 | gobject_type: 'GObject', 64 | image_type: 'Image', 65 | array_int_type: 'list[int]', 66 | array_double_type: 'list[float]', 67 | array_image_type: 'list[Image]', 68 | blob_type: 'str', 69 | source_type: 'Source', 70 | target_type: 'Target', 71 | } 72 | 73 | @staticmethod 74 | def gtype_to_python(gtype): 75 | """Map a gtype to the name of the Python type we use to represent it. 76 | 77 | """ 78 | 79 | fundamental = gobject_lib.g_type_fundamental(gtype) 80 | 81 | # enums can be strings or class members ... we want to generate a union 82 | # type 83 | if fundamental == GValue.genum_type: 84 | name = type_name(gtype) 85 | if name.startswith('Vips'): 86 | name = name[4:] 87 | return f"Union[str, {name}]" 88 | if gtype in GValue._gtype_to_python: 89 | return GValue._gtype_to_python[gtype] 90 | if fundamental in GValue._gtype_to_python: 91 | return GValue._gtype_to_python[fundamental] 92 | return '' 93 | 94 | @staticmethod 95 | def to_enum(gtype, value): 96 | """Turn a string into an enum value ready to be passed into libvips. 97 | 98 | """ 99 | 100 | if isinstance(value, str): 101 | enum_value = vips_lib.vips_enum_from_nick(b'pyvips', gtype, 102 | _to_bytes(value)) 103 | if enum_value < 0: 104 | raise Error('no value {0} in gtype {1} ({2})'. 105 | format(value, type_name(gtype), gtype)) 106 | else: 107 | enum_value = value 108 | 109 | return enum_value 110 | 111 | @staticmethod 112 | def from_enum(gtype, enum_value): 113 | """Turn an int back into an enum string. 114 | 115 | """ 116 | 117 | pointer = vips_lib.vips_enum_nick(gtype, enum_value) 118 | if pointer == ffi.NULL: 119 | raise Error('value not in enum') 120 | 121 | return _to_string(pointer) 122 | 123 | @staticmethod 124 | def to_flag(gtype, value): 125 | """Turn a string into a flag value ready to be passed into libvips. 126 | 127 | """ 128 | 129 | if isinstance(value, str): 130 | flag_value = vips_lib.vips_flags_from_nick(b'pyvips', gtype, 131 | _to_bytes(value)) 132 | if flag_value < 0: 133 | raise Error('no value {0} in gtype {1} ({2})'. 134 | format(value, type_name(gtype), gtype)) 135 | else: 136 | flag_value = value 137 | 138 | return flag_value 139 | 140 | def __init__(self): 141 | # allocate memory for the gvalue which will be freed on GC 142 | self.pointer = ffi.new('GValue *') 143 | # logger.debug('GValue.__init__: pointer = %s', self.pointer) 144 | 145 | # and tag it to be unset on GC as well 146 | self.gvalue = ffi.gc(self.pointer, gobject_lib.g_value_unset) 147 | # logger.debug('GValue.__init__: gvalue = %s', self.gvalue) 148 | 149 | def set_type(self, gtype): 150 | """Set the type of a GValue. 151 | 152 | GValues have a set type, fixed at creation time. Use set_type to set 153 | the type of a GValue before assigning to it. 154 | 155 | GTypes are 32 or 64-bit integers (depending on the platform). See 156 | type_find. 157 | 158 | """ 159 | 160 | gobject_lib.g_value_init(self.gvalue, gtype) 161 | 162 | def set(self, value): 163 | """Set a GValue. 164 | 165 | The value is converted to the type of the GValue, if possible, and 166 | assigned. 167 | 168 | """ 169 | 170 | # logger.debug('GValue.set: value = %s', value) 171 | 172 | gtype = self.gvalue.g_type 173 | fundamental = gobject_lib.g_type_fundamental(gtype) 174 | 175 | if gtype == GValue.gbool_type: 176 | gobject_lib.g_value_set_boolean(self.gvalue, value) 177 | elif gtype == GValue.gint_type: 178 | gobject_lib.g_value_set_int(self.gvalue, int(value)) 179 | elif gtype == GValue.guint64_type: 180 | gobject_lib.g_value_set_uint64(self.gvalue, value) 181 | elif gtype == GValue.gdouble_type: 182 | gobject_lib.g_value_set_double(self.gvalue, value) 183 | elif fundamental == GValue.genum_type: 184 | gobject_lib.g_value_set_enum(self.gvalue, 185 | GValue.to_enum(gtype, value)) 186 | elif fundamental == GValue.gflags_type: 187 | gobject_lib.g_value_set_flags(self.gvalue, 188 | GValue.to_flag(gtype, value)) 189 | elif gtype == GValue.gstr_type: 190 | gobject_lib.g_value_set_string(self.gvalue, _to_bytes(value)) 191 | elif gtype == GValue.refstr_type: 192 | vips_lib.vips_value_set_ref_string(self.gvalue, _to_bytes(value)) 193 | elif fundamental == GValue.gobject_type: 194 | gobject_lib.g_value_set_object(self.gvalue, value.pointer) 195 | elif gtype == GValue.array_int_type: 196 | if isinstance(value, numbers.Number): 197 | value = [value] 198 | 199 | array = ffi.new('int[]', value) 200 | vips_lib.vips_value_set_array_int(self.gvalue, array, len(value)) 201 | elif gtype == GValue.array_double_type: 202 | if isinstance(value, numbers.Number): 203 | value = [value] 204 | 205 | array = ffi.new('double[]', value) 206 | vips_lib.vips_value_set_array_double(self.gvalue, array, 207 | len(value)) 208 | elif gtype == GValue.array_image_type: 209 | if isinstance(value, pyvips.Image): 210 | value = [value] 211 | 212 | vips_lib.vips_value_set_array_image(self.gvalue, len(value)) 213 | array = vips_lib.vips_value_get_array_image(self.gvalue, ffi.NULL) 214 | for i, image in enumerate(value): 215 | gobject_lib.g_object_ref(image.pointer) 216 | array[i] = image.pointer 217 | elif gtype == GValue.blob_type: 218 | # we need to set the blob to a copy of the string that vips_lib 219 | # can own 220 | memory = glib_lib.g_malloc(len(value)) 221 | ffi.memmove(memory, value, len(value)) 222 | 223 | # In API mode, we use set_blob_free in a backwards compatible way. 224 | # For pre-8.6 libvipses, in ABI mode, we declare the type of the 225 | # free func in set_blob incorrectly so that we can pass g_free 226 | # at runtime without triggering an exception. 227 | if pyvips.API_mode or at_least_libvips(8, 6): 228 | vips_lib.vips_value_set_blob_free(self.gvalue, 229 | memory, len(value)) 230 | else: 231 | vips_lib.vips_value_set_blob(self.gvalue, 232 | glib_lib.g_free, 233 | memory, len(value)) 234 | else: 235 | raise Error('unsupported gtype for set {0}, fundamental {1}'. 236 | format(type_name(gtype), type_name(fundamental))) 237 | 238 | def get(self): 239 | """Get the contents of a GValue. 240 | 241 | The contents of the GValue are read out as a Python type. 242 | """ 243 | 244 | # logger.debug('GValue.get: self = %s', self) 245 | 246 | gtype = self.gvalue.g_type 247 | fundamental = gobject_lib.g_type_fundamental(gtype) 248 | 249 | result = None 250 | 251 | if gtype == GValue.gbool_type: 252 | result = bool(gobject_lib.g_value_get_boolean(self.gvalue)) 253 | elif gtype == GValue.gint_type: 254 | result = gobject_lib.g_value_get_int(self.gvalue) 255 | elif gtype == GValue.guint64_type: 256 | result = gobject_lib.g_value_get_uint64(self.gvalue) 257 | elif gtype == GValue.gdouble_type: 258 | result = gobject_lib.g_value_get_double(self.gvalue) 259 | elif fundamental == GValue.genum_type: 260 | return GValue.from_enum(gtype, 261 | gobject_lib.g_value_get_enum(self.gvalue)) 262 | elif fundamental == GValue.gflags_type: 263 | result = gobject_lib.g_value_get_flags(self.gvalue) 264 | elif gtype == GValue.gstr_type: 265 | pointer = gobject_lib.g_value_get_string(self.gvalue) 266 | 267 | if pointer != ffi.NULL: 268 | result = _to_string(pointer) 269 | elif gtype == GValue.refstr_type: 270 | psize = ffi.new('size_t *') 271 | pointer = vips_lib.vips_value_get_ref_string(self.gvalue, psize) 272 | 273 | # psize[0] will be number of bytes in string, but just assume it's 274 | # NULL-terminated 275 | result = _to_string(pointer) 276 | elif gtype == GValue.image_type: 277 | # g_value_get_object() will not add a ref ... that is 278 | # held by the gvalue 279 | go = gobject_lib.g_value_get_object(self.gvalue) 280 | vi = ffi.cast('VipsImage *', go) 281 | 282 | # we want a ref that will last with the life of the vimage: 283 | # this ref is matched by the unref that's attached to finalize 284 | # by Image() 285 | gobject_lib.g_object_ref(vi) 286 | 287 | result = pyvips.Image(vi) 288 | elif gtype == GValue.array_int_type: 289 | pint = ffi.new('int *') 290 | array = vips_lib.vips_value_get_array_int(self.gvalue, pint) 291 | 292 | result = [] 293 | for i in range(0, pint[0]): 294 | result.append(array[i]) 295 | elif gtype == GValue.array_double_type: 296 | pint = ffi.new('int *') 297 | array = vips_lib.vips_value_get_array_double(self.gvalue, pint) 298 | 299 | result = [] 300 | for i in range(0, pint[0]): 301 | result.append(array[i]) 302 | elif gtype == GValue.array_image_type: 303 | pint = ffi.new('int *') 304 | array = vips_lib.vips_value_get_array_image(self.gvalue, pint) 305 | 306 | result = [] 307 | for i in range(0, pint[0]): 308 | vi = array[i] 309 | gobject_lib.g_object_ref(vi) 310 | image = pyvips.Image(vi) 311 | result.append(image) 312 | elif gtype == GValue.blob_type: 313 | psize = ffi.new('size_t *') 314 | array = vips_lib.vips_value_get_blob(self.gvalue, psize) 315 | buf = ffi.cast('char*', array) 316 | 317 | result = ffi.unpack(buf, psize[0]) 318 | else: 319 | raise Error('unsupported gtype for get {0}'. 320 | format(type_name(gtype))) 321 | 322 | return result 323 | 324 | 325 | __all__ = ['GValue'] 326 | -------------------------------------------------------------------------------- /doc/intro.rst: -------------------------------------------------------------------------------- 1 | .. include global.rst 2 | 3 | Introduction 4 | ============ 5 | 6 | See the main libvips site for an introduction to the underlying library. These 7 | notes introduce the Python binding. 8 | 9 | https://libvips.github.io/libvips 10 | 11 | Example 12 | ------- 13 | 14 | This example loads a file, boosts the green channel, sharpens the image, 15 | and saves it back to disc again:: 16 | 17 | import pyvips 18 | 19 | image = pyvips.Image.new_from_file('some-image.jpg', access='sequential') 20 | image *= [1, 2, 1] 21 | mask = pyvips.Image.new_from_list([[-1, -1, -1], 22 | [-1, 16, -1], 23 | [-1, -1, -1] 24 | ], scale=8) 25 | image = image.conv(mask, precision='integer') 26 | image.write_to_file('x.jpg') 27 | 28 | Reading this example line by line, we have:: 29 | 30 | image = pyvips.Image.new_from_file('some-image.jpg', access='sequential') 31 | 32 | :meth:`.Image.new_from_file` can load any image file supported by libvips. 33 | When you load an image, only the header is fetched from the file. Pixels will 34 | not be read until you have built a pipeline of operations and connected it 35 | to an output. 36 | 37 | When you load, you can hint what type of access you will need. In this 38 | example, we will be accessing pixels top-to-bottom as we sweep through 39 | the image reading and writing, so `sequential` access mode is best for us. 40 | The default mode is ``random`` which allows for full random access to image 41 | pixels, but is slower and needs more memory. See :class:`.enums.Access` 42 | for details on the various modes available. 43 | 44 | You can also load formatted images from memory with 45 | :meth:`.Image.new_from_buffer`, create images that wrap C-style memory arrays 46 | held as Python buffers with :meth:`.Image.new_from_memory`, or make images 47 | from constants with :meth:`.Image.new_from_list`. You can also create custom 48 | sources and targets that link image processing pipelines to your own code, 49 | see `Custom sources and targets`_. 50 | 51 | The next line:: 52 | 53 | image *= [1, 2, 1] 54 | 55 | Multiplies the image by an array constant using one array element for each 56 | image band. This line assumes that the input image has three bands and will 57 | double the middle band. For RGB images, that's doubling green. 58 | 59 | There is a full set arithmetic operator `Overloads`_, so you can compute with 60 | entire images just as you would with single numbers. 61 | 62 | Next we have:: 63 | 64 | mask = pyvips.Image.new_from_list([[-1, -1, -1], 65 | [-1, 16, -1], 66 | [-1, -1, -1] 67 | ], scale = 8) 68 | image = image.conv(mask, precision = 'integer') 69 | 70 | :meth:`.new_from_list` creates an image from a list of lists. The 71 | scale is the amount to divide the image by after integer convolution. 72 | 73 | See the libvips API docs for ``vips_conv()`` (the operation 74 | invoked by :meth:`.Image.conv`) for details on the convolution operator. By 75 | default, it computes with a float mask, but ``integer`` is fine for this case, 76 | and is much faster. 77 | 78 | Finally:: 79 | 80 | image.write_to_file('x.jpg') 81 | 82 | :meth:`.write_to_file` writes an image back to the filesystem. It can 83 | write any format supported by vips: the file type is set from the filename 84 | suffix. You can also write formatted images to memory, or dump 85 | image data to a C-style array in a Python buffer. 86 | 87 | Metadata and attributes 88 | ----------------------- 89 | 90 | ``pyvips`` adds a :meth:`.__getattr__` handler to :class:`.Image` and to 91 | the Image metaclass, then uses it to look up unknown names in libvips. 92 | 93 | Names are first checked against the set of properties that libvips 94 | keeps for images, see :attr:`.width` and friends. If the name is not 95 | found there, ``pyvips`` searches the set of libvips operations, see the next 96 | section. 97 | 98 | As well as the core properties, you can read and write the metadata 99 | that libvips keeps for images with :meth:`.Image.get` and 100 | friends. For example:: 101 | 102 | image = pyvips.Image.new_from_file('some-image.jpg') 103 | ipct_string = image.get('ipct-data') 104 | exif_date_string = image.get('exif-ifd0-DateTime') 105 | 106 | Use :meth:`.get_fields` to get a list of all the field names you can use with 107 | :meth:`.Image.get`. 108 | 109 | libvips caches and shares images between different parts of your program. This 110 | means that you can't modify an image unless you are certain that you have 111 | the only reference to it. You can make a private copy of an image with 112 | ``copy``, for example:: 113 | 114 | new_image = image.copy(xres=12, yres=13) 115 | 116 | Now ``new_image`` is a private clone of ``image`` with ``xres`` and ``yres`` 117 | changed. 118 | 119 | Set image metadata with :meth:`.Image.set`. Use :meth:`.Image.copy` to make 120 | a private copy of the image first, for example:: 121 | 122 | new_image = image.copy().set('icc-profile-data', new_profile) 123 | 124 | Now ``new_image`` is a clone of ``image`` with a new ICC profile attached to 125 | it. 126 | 127 | NumPy and PIL 128 | ------------- 129 | 130 | You can use :meth:`.new_from_array` to create a pyvips image from a NumPy array 131 | or a PIL image. For example:: 132 | 133 | import pyvips 134 | import numpy as np 135 | 136 | a = (np.random.random((100, 100, 3)) * 255).astype(np.uint8) 137 | image = pyvips.Image.new_from_array(a) 138 | 139 | import PIL.Image 140 | pil_image = PIL.Image.new('RGB', (60, 30), color = 'red') 141 | image = pyvips.Image.new_from_array(pil_image) 142 | 143 | Going the other direction, a conversion from a pyvips image to a NumPy array can 144 | be obtained with the :meth:`.numpy()` method of the :class:`pyvips.Image` class 145 | or from the numpy side with :func:`numpy.asarray`. This is a fast way to load 146 | many image formats:: 147 | 148 | import pyvips 149 | import numpy as np 150 | 151 | image = pyvips.Image.new_from_file('some-image.jpg') 152 | a1 = image.numpy() 153 | a2 = np.asarray(image) 154 | 155 | assert np.array_equal(a1, a2) 156 | 157 | The :meth:`PIL.Image.fromarray` method can be used to convert a pyvips image 158 | to a PIL image via a NumPy array:: 159 | 160 | import pyvips 161 | import PIL.Image 162 | image = pyvips.Image.black(100, 100, bands=3) 163 | pil_image = PIL.Image.fromarray(image.numpy()) 164 | 165 | 166 | Calling libvips operations 167 | -------------------------- 168 | 169 | Unknown names which are not image properties are looked up as libvips 170 | operations. For example, the libvips operation ``add``, which appears in C as 171 | ``vips_add()``, appears in Python as :meth:`.Image.add`. 172 | 173 | The operation's list of required arguments is searched and the first input 174 | image is set to the value of ``self``. Operations which do not take an input 175 | image, such as :meth:`.Image.black`, appear as class methods. The 176 | remainder of the arguments you supply in the function call are used to set 177 | the other required input arguments. Any trailing keyword arguments are used 178 | to set options on the underlying libvips operation. 179 | 180 | The result is the required output argument if there is only one result, 181 | or a list of values if the operation produces several results. If the 182 | operation has optional output objects, they are returned as a final 183 | Python dictionary. 184 | 185 | For example, :meth:`.Image.min`, the vips operation that searches an 186 | image for the minimum value, has a large number of optional arguments. You 187 | can use it to find the minimum value like this:: 188 | 189 | min_value = image.min() 190 | 191 | You can ask it to return the position of the minimum with `:x` and `:y`:: 192 | 193 | min_value, opts = image.min(x=True, y=True) 194 | x_pos = opts['x'] 195 | y_pos = opts['y'] 196 | 197 | Now ``x_pos`` and ``y_pos`` will have the coordinates of the minimum value. 198 | There's actually a convenience method for this, :meth:`.minpos`. 199 | 200 | You can also ask for the top *n* minimum, for example:: 201 | 202 | min_value, opts = min(size=10, x_array=True, y_array=True) 203 | x_pos = opts['x_array'] 204 | y_pos = opts['y_array'] 205 | 206 | Now ``x_pos`` and ``y_pos`` will be 10-element arrays. 207 | 208 | Because operations are member functions and return the result image, you can 209 | chain them. For example, you can write:: 210 | 211 | result_image = image.real().cos() 212 | 213 | to calculate the cosine of the real part of a complex image. There is 214 | also a full set of arithmetic `Overloads`_. 215 | 216 | libvips types are automatically wrapped. The binding looks at the type 217 | of argument required by the operation and converts the value you supply, 218 | when it can. For example, :meth:`.Image.linear` takes a 219 | ``VipsArrayDouble`` as an argument for the set of constants to use for 220 | multiplication. You can supply this value as an integer, a float, or some 221 | kind of compound object and it will be converted for you. You can write:: 222 | 223 | result_image = image.linear(1, 3) 224 | result_image = image.linear(12.4, 13.9) 225 | result_image = image.linear([1, 2, 3], [4, 5, 6]) 226 | result_image = image.linear(1, [4, 5, 6]) 227 | 228 | And so on. A set of overloads are defined for :meth:`.Image.linear`, 229 | see below. 230 | 231 | If an operation takes several input images, you can use a constant for all but 232 | one of them and the wrapper will expand the constant to an image for you. For 233 | example, :meth:`.ifthenelse` uses a condition image to pick pixels 234 | between a then and an else image:: 235 | 236 | result_image = condition_image.ifthenelse(then_image, else_image) 237 | 238 | You can use a constant instead of either the then or the else parts and it 239 | will be expanded to an image for you. If you use a constant for both then and 240 | else, it will be expanded to match the condition image. For example:: 241 | 242 | result_image = condition_image.ifthenelse([0, 255, 0], [255, 0, 0]) 243 | 244 | Will make an image where true pixels are green and false pixels are red. 245 | 246 | This is useful for :meth:`.bandjoin`, the thing to join two or more 247 | images up bandwise. You can write:: 248 | 249 | rgba = rgb.bandjoin(255) 250 | 251 | to append a constant 255 band to an image, perhaps to add an alpha channel. Of 252 | course you can also write:: 253 | 254 | result_image = image1.bandjoin(image2) 255 | result_image = image1.bandjoin([image2, image3]) 256 | result_image = pyvips.Image.bandjoin([image1, image2, image3]) 257 | result_image = image1.bandjoin([image2, 255]) 258 | 259 | and so on. 260 | 261 | Logging and warnings 262 | -------------------- 263 | 264 | The module uses ``logging`` to log warnings from libvips, and debug messages 265 | from the module itself. Some warnings are important, for example truncated 266 | files, and you might want to see them. 267 | 268 | Add these lines somewhere near the start of your program:: 269 | 270 | import logging 271 | logging.basicConfig(level=logging.WARNING) 272 | 273 | 274 | Exceptions 275 | ---------- 276 | 277 | The wrapper spots errors from vips operations and raises the :class:`.Error` 278 | exception. You can catch it in the usual way. 279 | 280 | Enums 281 | ----- 282 | 283 | The libvips enums, such as ``VipsBandFormat``, appear in pyvips as strings 284 | like ``'uchar'``. They are documented as a set of classes for convenience, see 285 | :class:`.Access`, for example. 286 | 287 | Overloads 288 | --------- 289 | 290 | The wrapper defines the usual set of arithmetic, boolean and relational 291 | overloads on image. You can mix images, constants and lists of constants 292 | freely. For example, you can write:: 293 | 294 | result_image = ((image * [1, 2, 3]).abs() < 128) | 4 295 | 296 | Expansions 297 | ---------- 298 | 299 | Some vips operators take an enum to select an action, for example 300 | :meth:`.Image.math` can be used to calculate sine of every pixel 301 | like this:: 302 | 303 | result_image = image.math('sin') 304 | 305 | This is annoying, so the wrapper expands all these enums into separate members 306 | named after the enum value. So you can also write:: 307 | 308 | result_image = image.sin() 309 | 310 | Convenience functions 311 | --------------------- 312 | 313 | The wrapper defines a few extra useful utility functions: 314 | :meth:`.bandsplit`, :meth:`.maxpos`, :meth:`.minpos`, 315 | :meth:`.median`. 316 | 317 | Tracking and interrupting computation 318 | ------------------------------------- 319 | 320 | You can attach progress handlers to images to watch the progress of 321 | computation. 322 | 323 | For example:: 324 | 325 | image = pyvips.Image.black(1, 500) 326 | image.set_progress(True) 327 | image.signal_connect('preeval', preeval_handler) 328 | image.signal_connect('eval', eval_handler) 329 | image.signal_connect('posteval', posteval_handler) 330 | image.avg() 331 | 332 | Handlers are given a `progress` object containing a number of useful fields. 333 | For example:: 334 | 335 | def eval_handler(image, progress): 336 | print(f' run = {progress.run} (seconds of run time)') 337 | print(f' eta = {progress.eta} (estimated seconds left)') 338 | print(f' tpels = {progress.tpels} (total number of pels)') 339 | print(f' npels = {progress.npels} (number of pels computed so far)') 340 | print(f' percent = {progress.percent} (percent complete)') 341 | 342 | Use :meth:`.Image.set_kill` on the image to stop computation early. 343 | 344 | For example:: 345 | 346 | def eval_handler(image, progress): 347 | if progress.percent > 50: 348 | image.set_kill(True) 349 | 350 | Custom sources and targets 351 | -------------------------- 352 | 353 | You can load and save images to and from :class:`.Source` and 354 | :class:`.Target`. 355 | 356 | For example:: 357 | 358 | source = pyvips.Source.new_from_file("some/file/name") 359 | image = pyvips.Image.new_from_source(source, "", access="sequential") 360 | target = pyvips.Target.new_to_file("some/file/name") 361 | image.write_to_target(target, ".png") 362 | 363 | Sources and targets can be files, descriptors (eg. pipes) and areas of memory. 364 | 365 | You can define :class:`.SourceCustom` and :class:`.TargetCustom` too. 366 | 367 | For example:: 368 | 369 | input_file = open(sys.argv[1], "rb") 370 | 371 | def read_handler(size): 372 | return input_file.read(size) 373 | 374 | source = pyvips.SourceCustom() 375 | source.on_read(read_handler) 376 | 377 | output_file = open(sys.argv[2], "wb") 378 | 379 | def write_handler(chunk): 380 | return output_file.write(chunk) 381 | 382 | target = pyvips.TargetCustom() 383 | target.on_write(write_handler) 384 | 385 | image = pyvips.Image.new_from_source(source, '', access='sequential') 386 | image.write_to_target(target, '.png') 387 | 388 | You can also define seek and finish handlers, see the docs. 389 | 390 | Automatic documentation 391 | ----------------------- 392 | 393 | The bulk of these API docs are generated automatically by 394 | :meth:`.Operation.generate_sphinx_all`. It examines libvips and writes a 395 | summary of each operation and the arguments and options that that operation 396 | expects. 397 | 398 | Use the C API docs for more detail: 399 | 400 | https://libvips.github.io/libvips/API/current 401 | 402 | Draw operations 403 | --------------- 404 | 405 | Paint operations like :meth:`.Image.draw_circle` and 406 | :meth:`.Image.draw_line` modify their input image. This makes them 407 | hard to use with the rest of libvips: you need to be very careful about 408 | the order in which operations execute or you can get nasty crashes. 409 | 410 | The wrapper spots operations of this type and makes a private copy of the 411 | image in memory before calling the operation. This stops crashes, but it does 412 | make it inefficient. If you draw 100 lines on an image, for example, you'll 413 | copy the image 100 times. The wrapper does make sure that memory is recycled 414 | where possible, so you won't have 100 copies in memory. 415 | 416 | If you want to avoid the copies, you'll need to call drawing operations 417 | yourself. 418 | 419 | -------------------------------------------------------------------------------- /pyvips/vdecls.py: -------------------------------------------------------------------------------- 1 | # all the C decls for pyvips 2 | 3 | # we keep these together to make switching between ABI and API modes simpler 4 | 5 | # we have to pass in the libvips version, since it can come from either 6 | # pkg-config in compile.py (in API mode) or libvips itself in __init__.py 7 | # (in ABI mode) 8 | 9 | import sys 10 | 11 | 12 | def _at_least(features, x, y): 13 | return features['major'] > x or (features['major'] == x and 14 | features['minor'] >= y) 15 | 16 | 17 | def cdefs(features): 18 | """Return the C API declarations for libvips. 19 | 20 | features is a dict with the features we want. Some features were only 21 | added in later libvips, for example, and some need to be disabled in 22 | some FFI modes. 23 | 24 | """ 25 | 26 | # we need the glib names for these types 27 | code = ''' 28 | typedef uint32_t guint32; 29 | typedef int32_t gint32; 30 | typedef uint64_t guint64; 31 | typedef int64_t gint64; 32 | ''' 33 | 34 | # apparently the safest way to do this 35 | is_64bits = sys.maxsize > 2 ** 32 36 | 37 | # GType is an int the size of a pointer ... I don't think we can just use 38 | # size_t, sadly 39 | if is_64bits: 40 | code += ''' 41 | typedef guint64 GType; 42 | ''' 43 | else: 44 | code += ''' 45 | typedef guint32 GType; 46 | ''' 47 | 48 | # ... means opaque 49 | code += ''' 50 | typedef void (*GLogFunc) (const char* log_domain, 51 | int log_level, 52 | const char* message, void* user_data); 53 | int g_log_set_handler (const char* log_domain, 54 | int log_levels, 55 | GLogFunc log_func, void* user_data); 56 | 57 | extern "Python" void _log_handler_callback (const char*, int, 58 | const char*, void*); 59 | 60 | void g_log_remove_handler (const char* log_domain, int handler_id); 61 | 62 | typedef ... VipsImage; 63 | 64 | void* g_malloc (size_t size); 65 | void g_free (void* data); 66 | 67 | void vips_leak_set (int leak); 68 | 69 | GType vips_type_find (const char* basename, const char* nickname); 70 | const char* vips_nickname_find (GType type); 71 | 72 | const char* g_type_name (GType gtype); 73 | GType g_type_from_name (const char* name); 74 | 75 | typedef void* (*VipsTypeMap2Fn) (GType type, void* a, void* b); 76 | void* vips_type_map (GType base, void* fn, void* a, void* b); 77 | 78 | void vips_shutdown (void); 79 | 80 | const char* vips_error_buffer (void); 81 | void vips_error_clear (void); 82 | void vips_error_freeze (void); 83 | void vips_error_thaw (void); 84 | 85 | typedef struct _GValue { 86 | GType g_type; 87 | union { 88 | guint64 v_uint64; 89 | 90 | // more 91 | } data[2]; 92 | } GValue; 93 | 94 | void g_value_init (GValue* value, GType gtype); 95 | void g_value_unset (GValue* value); 96 | GType g_type_fundamental (GType gtype); 97 | 98 | int vips_enum_from_nick (const char* domain, 99 | GType gtype, const char* str); 100 | int vips_flags_from_nick (const char* domain, 101 | GType gtype, const char* nick); 102 | const char *vips_enum_nick (GType gtype, int value); 103 | 104 | void g_value_set_boolean (GValue* value, int v_boolean); 105 | void g_value_set_int (GValue* value, int i); 106 | void g_value_set_uint64 (GValue* value, guint64 ull); 107 | void g_value_set_double (GValue* value, double d); 108 | void g_value_set_enum (GValue* value, int e); 109 | void g_value_set_flags (GValue* value, unsigned int f); 110 | void g_value_set_string (GValue* value, const char* str); 111 | void vips_value_set_ref_string (GValue* value, const char* str); 112 | void g_value_set_object (GValue* value, void* object); 113 | void vips_value_set_array_double (GValue* value, 114 | const double* array, int n ); 115 | void vips_value_set_array_int (GValue* value, 116 | const int* array, int n ); 117 | void vips_value_set_array_image (GValue *value, int n); 118 | 119 | int g_value_get_boolean (const GValue* value); 120 | int g_value_get_int (GValue* value); 121 | guint64 g_value_get_uint64 (GValue* value); 122 | double g_value_get_double (GValue* value); 123 | int g_value_get_enum (GValue* value); 124 | unsigned int g_value_get_flags (GValue* value); 125 | const char* g_value_get_string (GValue* value); 126 | const char* vips_value_get_ref_string (const GValue* value, 127 | size_t* length); 128 | void* g_value_get_object (GValue* value); 129 | double* vips_value_get_array_double (const GValue* value, int* n); 130 | int* vips_value_get_array_int (const GValue* value, int* n); 131 | VipsImage** vips_value_get_array_image (const GValue* value, int* n); 132 | void* vips_value_get_blob (const GValue* value, size_t* length); 133 | 134 | // need to make some of these by hand 135 | GType vips_interpretation_get_type (void); 136 | GType vips_operation_flags_get_type (void); 137 | GType vips_band_format_get_type (void); 138 | 139 | typedef ... GData; 140 | 141 | typedef struct _GTypeClass { 142 | GType g_type; 143 | } GTypeClass; 144 | 145 | typedef struct _GTypeInstance { 146 | GTypeClass *g_class; 147 | } GTypeInstance; 148 | 149 | typedef struct _GObject { 150 | GTypeInstance g_type_instance; 151 | 152 | unsigned int ref_count; 153 | GData *qdata; 154 | } GObject; 155 | 156 | typedef struct _GParamSpec { 157 | GTypeInstance g_type_instance; 158 | 159 | const char* name; 160 | unsigned int flags; 161 | GType value_type; 162 | GType owner_type; 163 | 164 | // private, but cffi in API mode needs these to be able to get the 165 | // offset of any member 166 | char* _nick; 167 | char* _blurb; 168 | GData* qdata; 169 | unsigned int ref_count; 170 | unsigned int param_id; 171 | } GParamSpec; 172 | 173 | typedef struct _GEnumValue { 174 | int value; 175 | 176 | const char *value_name; 177 | const char *value_nick; 178 | } GEnumValue; 179 | 180 | typedef struct _GEnumClass { 181 | GTypeClass g_type_class; 182 | 183 | int minimum; 184 | int maximum; 185 | unsigned int n_values; 186 | GEnumValue *values; 187 | } GEnumClass; 188 | 189 | typedef struct _GFlagsValue { 190 | unsigned int value; 191 | 192 | const char *value_name; 193 | const char *value_nick; 194 | } GFlagsValue; 195 | 196 | typedef struct _GFlagsClass { 197 | GTypeClass g_type_class; 198 | 199 | unsigned int mask; 200 | unsigned int n_values; 201 | GFlagsValue *values; 202 | } GFlagsClass; 203 | 204 | void* g_type_class_ref (GType type); 205 | 206 | void* g_object_new (GType type, void*); 207 | void g_object_ref (void* object); 208 | void g_object_unref (void* object); 209 | 210 | void g_object_set_property (GObject* object, 211 | const char *name, GValue* value); 212 | void g_object_get_property (GObject* object, 213 | const char* name, GValue* value); 214 | 215 | void vips_image_invalidate_all (VipsImage* image); 216 | 217 | typedef void (*GCallback)(void); 218 | typedef void (*GClosureNotify)(void* data, struct _GClosure *); 219 | long g_signal_connect_data (GObject* object, 220 | const char* detailed_signal, 221 | GCallback c_handler, 222 | void* data, 223 | GClosureNotify destroy_data, 224 | int connect_flags); 225 | 226 | extern "Python" void _marshal_image_progress (VipsImage*, 227 | void*, void*); 228 | 229 | void vips_image_set_progress (VipsImage* image, int progress); 230 | void vips_image_set_kill (VipsImage* image, int kill); 231 | 232 | typedef ... GTimer; 233 | 234 | typedef struct _VipsProgress { 235 | VipsImage* im; 236 | 237 | int run; 238 | int eta; 239 | gint64 tpels; 240 | gint64 npels; 241 | int percent; 242 | GTimer* start; 243 | } VipsProgress; 244 | 245 | typedef ... VipsObject; 246 | 247 | typedef ... VipsObjectClass; 248 | 249 | typedef struct _VipsArgument { 250 | GParamSpec *pspec; 251 | } VipsArgument; 252 | 253 | typedef struct _VipsArgumentInstance { 254 | VipsArgument parent; 255 | 256 | // more 257 | } VipsArgumentInstance; 258 | 259 | typedef enum _VipsArgumentFlags { 260 | VIPS_ARGUMENT_NONE = 0, 261 | VIPS_ARGUMENT_REQUIRED = 1, 262 | VIPS_ARGUMENT_CONSTRUCT = 2, 263 | VIPS_ARGUMENT_SET_ONCE = 4, 264 | VIPS_ARGUMENT_SET_ALWAYS = 8, 265 | VIPS_ARGUMENT_INPUT = 16, 266 | VIPS_ARGUMENT_OUTPUT = 32, 267 | VIPS_ARGUMENT_DEPRECATED = 64, 268 | VIPS_ARGUMENT_MODIFY = 128 269 | } VipsArgumentFlags; 270 | 271 | typedef struct _VipsArgumentClass { 272 | VipsArgument parent; 273 | 274 | VipsObjectClass *object_class; 275 | VipsArgumentFlags flags; 276 | int priority; 277 | unsigned int offset; 278 | } VipsArgumentClass; 279 | 280 | int vips_object_get_argument (VipsObject* object, 281 | const char *name, GParamSpec** pspec, 282 | VipsArgumentClass** argument_class, 283 | VipsArgumentInstance** argument_instance); 284 | 285 | void vips_object_print_all (void); 286 | 287 | int vips_object_set_from_string (VipsObject* object, 288 | const char* options); 289 | 290 | const char* vips_object_get_description (VipsObject* object); 291 | 292 | const char* g_param_spec_get_blurb (GParamSpec* pspec); 293 | 294 | const char* vips_foreign_find_load (const char* name); 295 | const char* vips_foreign_find_load_buffer (const void* data, 296 | size_t size); 297 | const char* vips_foreign_find_save (const char* name); 298 | const char* vips_foreign_find_save_buffer (const char* suffix); 299 | 300 | VipsImage* vips_image_new_matrix_from_array (int width, int height, 301 | const double* array, int size); 302 | VipsImage* vips_image_new_from_memory (const void* data, size_t size, 303 | int width, int height, int bands, int format); 304 | 305 | VipsImage* vips_image_copy_memory (VipsImage* image); 306 | 307 | GType vips_image_get_typeof (const VipsImage* image, 308 | const char* name); 309 | int vips_image_get (const VipsImage* image, 310 | const char* name, GValue* value_copy); 311 | void vips_image_set (VipsImage* image, 312 | const char* name, GValue* value); 313 | int vips_image_remove (VipsImage* image, const char* name); 314 | 315 | char* vips_filename_get_filename (const char* vips_filename); 316 | char* vips_filename_get_options (const char* vips_filename); 317 | 318 | VipsImage* vips_image_new_temp_file (const char* format); 319 | 320 | int vips_image_write (VipsImage* image, VipsImage* out); 321 | void* vips_image_write_to_memory (VipsImage* in, size_t* size_out); 322 | 323 | typedef ... VipsInterpolate; 324 | 325 | VipsInterpolate* vips_interpolate_new (const char* name); 326 | 327 | typedef ... VipsOperation; 328 | 329 | VipsOperation* vips_operation_new (const char* name); 330 | 331 | typedef void* (*VipsArgumentMapFn) (VipsObject* object, 332 | GParamSpec* pspec, 333 | VipsArgumentClass* argument_class, 334 | VipsArgumentInstance* argument_instance, 335 | void* a, void* b); 336 | 337 | void* vips_argument_map (VipsObject* object, 338 | VipsArgumentMapFn fn, void* a, void* b); 339 | 340 | typedef ... VipsRegion; 341 | 342 | VipsRegion* vips_region_new (VipsImage*); 343 | 344 | VipsOperation* vips_cache_operation_build (VipsOperation* operation); 345 | void vips_object_unref_outputs (VipsObject* object); 346 | 347 | int vips_operation_get_flags (VipsOperation* operation); 348 | 349 | void vips_cache_set_max (int max); 350 | void vips_cache_set_max_mem (size_t max_mem); 351 | void vips_cache_set_max_files (int max_files); 352 | void vips_cache_set_trace (int trace); 353 | 354 | int vips_cache_get_max(); 355 | int vips_cache_get_size(); 356 | size_t vips_cache_get_max_mem(); 357 | int vips_cache_get_max_files(); 358 | 359 | ''' 360 | 361 | # we must only define this in ABI mode ... in API mode we use 362 | # vips_value_set_blob_free in a backwards compatible way 363 | if not features['api']: 364 | code += ''' 365 | typedef void (*FreeFn)(void* a); 366 | void vips_value_set_blob (GValue* value, 367 | FreeFn free_fn, void* data, size_t length); 368 | ''' 369 | 370 | if _at_least(features, 8, 5): 371 | code += ''' 372 | char** vips_image_get_fields (VipsImage* image); 373 | int vips_image_hasalpha (VipsImage* image); 374 | 375 | ''' 376 | 377 | if _at_least(features, 8, 6): 378 | code += ''' 379 | GType vips_blend_mode_get_type (void); 380 | void vips_value_set_blob_free (GValue* value, 381 | void* data, size_t length); 382 | 383 | ''' 384 | 385 | if _at_least(features, 8, 7): 386 | code += ''' 387 | int vips_object_get_args (VipsObject* object, 388 | const char*** names, int** flags, int* n_args); 389 | 390 | ''' 391 | 392 | if _at_least(features, 8, 8): 393 | code += ''' 394 | char** vips_foreign_get_suffixes (void); 395 | 396 | void* vips_region_fetch (VipsRegion*, int, int, int, int, 397 | size_t* length); 398 | int vips_region_width (VipsRegion*); 399 | int vips_region_height (VipsRegion*); 400 | int vips_image_get_page_height (VipsImage*); 401 | int vips_image_get_n_pages (VipsImage*); 402 | 403 | ''' 404 | 405 | if _at_least(features, 8, 9): 406 | code += ''' 407 | typedef ... VipsConnection; 408 | 409 | const char* vips_connection_filename (VipsConnection* stream); 410 | const char* vips_connection_nick (VipsConnection* stream); 411 | 412 | typedef ... VipsSource; 413 | 414 | VipsSource* vips_source_new_from_descriptor (int descriptor); 415 | VipsSource* vips_source_new_from_file (const char* filename); 416 | VipsSource* vips_source_new_from_memory (const void* data, 417 | size_t size); 418 | 419 | typedef ... VipsSourceCustom; 420 | 421 | VipsSourceCustom* vips_source_custom_new (void); 422 | 423 | extern "Python" gint64 _marshal_read (VipsSource*, 424 | void*, gint64, void*); 425 | extern "Python" gint64 _marshal_seek (VipsSource*, 426 | gint64, int, void*); 427 | 428 | typedef ... VipsTarget; 429 | 430 | VipsTarget* vips_target_new_to_descriptor (int descriptor); 431 | VipsTarget* vips_target_new_to_file (const char* filename); 432 | VipsTarget* vips_target_new_to_memory (void); 433 | 434 | typedef ... VipsTargetCustom; 435 | 436 | VipsTargetCustom* vips_target_custom_new (void); 437 | 438 | extern "Python" gint64 _marshal_write (VipsTarget*, 439 | void*, gint64, void*); 440 | extern "Python" void _marshal_finish (VipsTarget*, 441 | void*); 442 | 443 | const char* vips_foreign_find_load_source (VipsSource *source); 444 | const char* vips_foreign_find_save_target (const char* suffix); 445 | 446 | ''' 447 | 448 | if _at_least(features, 8, 13): 449 | code += ''' 450 | extern "Python" int _marshal_end (VipsTarget*, 451 | void*); 452 | 453 | void vips_block_untrusted_set (int state); 454 | void vips_operation_block_set (const char *name, int state); 455 | 456 | ''' 457 | 458 | if _at_least(features, 8, 18): 459 | code += ''' 460 | VipsImage *vips_image_get_gainmap(VipsImage *image); 461 | 462 | ''' 463 | 464 | # we must only define these in API mode ... in ABI mode we need to call 465 | # these things earlier 466 | if features['api']: 467 | code += ''' 468 | int vips_init (const char* argv0); 469 | int vips_version (int flag); 470 | ''' 471 | 472 | # ... means inherit from C defines 473 | code += ''' 474 | #define VIPS_MAJOR_VERSION ... 475 | #define VIPS_MINOR_VERSION ... 476 | #define VIPS_MICRO_VERSION ... 477 | ''' 478 | 479 | # add contents of features as a comment ... handy for debugging 480 | for key, value in features.items(): 481 | code += f'//{key} = {value}\n' 482 | 483 | return code 484 | 485 | 486 | __all__ = ['cdefs'] 487 | --------------------------------------------------------------------------------