├── 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 |
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 |
--------------------------------------------------------------------------------