diff --git a/.github/workflows/build_config.json b/.github/workflows/build_config.json index 1616d90..16e6fb1 100644 --- a/.github/workflows/build_config.json +++ b/.github/workflows/build_config.json @@ -1,12 +1,12 @@ { "linux-x86_64": { - "pylon_sdks": ["7.5.0.15658"] + "pylon_sdks": ["8.0.1.16188"] }, "linux-aarch64": { - "pylon_sdks": ["7.5.0.15658"] + "pylon_sdks": ["8.0.1.16188"] }, "windows": { - "pylon_sdks": ["7.5.0.15658"] + "pylon_sdks": ["8.0.1.16188"] }, "macos": { "pylon_sdks": ["7.3.1.0011"] diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7020b97..e9776b6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -94,7 +94,7 @@ jobs: strategy: fail-fast: false matrix: - a: [cp39, cp3_10, cp3_11, cp3_12] + a: [cp39] p: [manylinux_2_31_x86_64, manylinux_2_31_aarch64] env: @@ -158,7 +158,7 @@ jobs: - name: Install Python uses: actions/setup-python@v5 with: - python-version: "3.9 - 3.12" + python-version: "3.9" - uses: actions/download-artifact@v4 with: @@ -172,11 +172,11 @@ jobs: run: | cd pylon-installer ren Basler-pylon-*.exe Basler-pylon.exe - $process = Start-Process Basler-pylon.exe -Wait -ArgumentList '/quiet /install=GigE_Runtime;USB_Runtime;Camera_Link_Runtime;GenTL_Consumer_Support;CamEmu_Support;SDKs;DataProcessing_SDK;DataProcessing_vTools' -PassThru + $process = Start-Process Basler-pylon.exe -Wait -ArgumentList '/quiet /install=GigE_Runtime;USB_Runtime;Camera_Link_Runtime;GenTL_Consumer_Support;CamEmu_Support;SDKs;DataProcessing_SDK;DataProcessing_vTools;DataProcessing_AI;DataProcessing_CREATOR' -PassThru Write-Host "Process finished with return code:" $process.ExitCode - name: Build wheels - uses: pypa/cibuildwheel@v2.19 + uses: pypa/cibuildwheel@v2.21.3 - uses: actions/upload-artifact@v4 with: @@ -210,7 +210,7 @@ jobs: - name: Install Python uses: actions/setup-python@v5 with: - python-version: "3.9 - 3.12" + python-version: "3.9" - uses: actions/download-artifact@v4 with: @@ -226,7 +226,7 @@ jobs: hdiutil detach /Volumes/pylon\ *\ Camera\ Software\ Suite - name: Build wheels - uses: pypa/cibuildwheel@v2.19 + uses: pypa/cibuildwheel@v2.21.3 env: PYLON_FRAMEWORK_ARM64: /Library/Frameworks PYLON_FRAMEWORK_X86_64: /Library/Frameworks diff --git a/README.md b/README.md index 70a1ec2..8ab7f9a 100644 --- a/README.md +++ b/README.md @@ -113,12 +113,12 @@ Please note that the pylon Camera Software Suite may support different operating For latest information on pylon refer to: https://www.baslerweb.com/en/software/pylon/ In addition, check the release notes of your pylon installation. For instance: -* pylon Camera Software Suite 7.5.0 supports Windows 10/11 64 bit, Linux x86_64 and Linux aarch64 with glibc version >= 2.31 or newer. +* pylon Camera Software Suite 8.0.1 supports Windows 10/11 64 bit, Linux x86_64 and Linux aarch64 with glibc version >= 2.31 or newer. * pylon vTools are supported on pylon 7.0.0 and newer. * pylon vTools are supported on pypylon 3.0 and newer only on Windows 10/11 64 bit, Linux x86_64 and Linux aarch64. * For pylon vTools that require a license refer to: https://www.baslerweb.com/en/software/pylon-vtools/ * CXP-12: To use CXP with pypylon >= 4.0.0 you need to install the CXP GenTL producer and drivers using the pylon Camera Software Suite setup. -* For accessing Basler 3D cameras, e.g. Basler blaze, installation of pylon Camera Software Suite 7.5.0 +* For accessing Basler 3D cameras, e.g. Basler blaze, installation of pylon Camera Software Suite 8.0.1 and the latest pylon Supplementary Package for blaze is required. ## Binary Installation @@ -152,7 +152,7 @@ You need a few more things to compile pypylon: * An installation of pylon SDK for your platform * A compiler for your system (Visual Studio on Windows, gcc on linux, xCode commandline tools on macOS) * Python development files (e.g. `sudo apt install python-dev` on linux) - * [swig](http://www.swig.org) >= 4.0 + * [swig](http://www.swig.org) >= 4.2 * For all 64bit platforms you can install the tool via `pip install swig` To build pypylon from source: diff --git a/changelog.txt b/changelog.txt index 71d55c4..dee3240 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,7 +1,16 @@ +Version 4.x.x + - Date xxxx-xx-xx + - Updated to pylon Camera Software Suite 8.0.1 on linux and windows. + This contains pylon C++ SDK 9.0.3 and pylon Data Processing C++ SDK 3.0.1. + - We now use python 3.9 limited api to provide multi abi version wheels of pypylon. + - Removed PylonImage.AttachUserBuffer + - Added PylonImage.AttachMemoryView + - Added PylonImage.AttachArray + Version 4.0.0 - Date 2024-07-08 - Updated to pylon Camera Software Suite 7.5 on linux and windows. - This contains pylon SDK 8.0.0 and pylon Data Processing SDK 2.0.0. + This contains pylon C++ SDK 8.0.0 and pylon Data Processing C++ SDK 2.0.0. - Removed pylon CXP GenTL Producer files from windows wheel packages. The CXP-12 support is automatically available when the pylon Camera Software Suite 7.5 with CXP is installed. diff --git a/pyproject.toml b/pyproject.toml index 34732af..e83e2ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools>=42", "swig>=4.2", "wheel"] +requires = ["setuptools>=42,<72", "swig>=4.2", "wheel"] build-backend = "setuptools.build_meta" [tool.cibuildwheel] @@ -9,7 +9,7 @@ test-requires = "pytest numpy" [tool.cibuildwheel.windows] archs = "AMD64" -build = "*-win_*" +build = "cp39-win_*" before-all = 'echo "Building: %CIBW_BUILD%"' test-command = [ ''' @@ -19,7 +19,7 @@ test-command = [ [tool.cibuildwheel.macos] archs = "x86_64 arm64" -build = "*-macosx_*" +build = "cp39-macosx_*" before-all = 'echo "Building: $CIBW_BUILD"' repair-wheel-command = "" test-command = [ diff --git a/scripts/build/Dockerfile.debian b/scripts/build/Dockerfile.debian index 92d337d..5f680a3 100644 --- a/scripts/build/Dockerfile.debian +++ b/scripts/build/Dockerfile.debian @@ -1,4 +1,3 @@ -ARG QEMU_TARGET_ARCH ARG DOCKER_BASE_IMAGE #the following lines are used to get a qemu binary only with docker tools @@ -6,6 +5,7 @@ FROM multiarch/qemu-user-static:4.2.0-6 as qemu FROM $DOCKER_BASE_IMAGE ARG CMD_WRAPPER +ARG QEMU_TARGET_ARCH COPY --from=qemu /usr/bin/* /usr/bin/ # Quick fix for the now archived debian jessie. Security updates are also no longer provided for arm64. @@ -20,13 +20,10 @@ RUN if cat /etc/debian_version | grep -q "8\." ; then \ # auditwheel is limited to 5.1.2 because since 5.2.1 it requires patchelf >= 0.14 which is not available on our debian base images RUN pip install wheel 'auditwheel<=5.1.2' -# build a new swig -RUN mkdir /build && \ - cd /build && \ - wget http://prdownloads.sourceforge.net/swig/swig-4.0.1.tar.gz && \ - tar -xzf swig-4.0.1.tar.gz && cd swig-4.0.1 && \ - ./configure --with-python3 && make -j2 && make install && \ - rm -rf /build +# install swig from pypi +RUN pip install swig>=4.2 +# install setuptools +RUN pip install "setuptools<72" --upgrade # numpy is required for the pypylon unittests # currently disabled because the numpy install exceeds the current travis max duration @@ -34,7 +31,9 @@ RUN mkdir /build && \ # one genicam unittest requires a french locale # patchelf, unzip are needed for auditwheel -RUN apt-get update && apt-get install -y locales patchelf unzip\ +# symlink for libicuuc.so.66 needed for pylon +RUN apt-get update && apt-get install -y locales patchelf unzip libicu67\ + && ln -s /usr/lib/$QEMU_TARGET_ARCH-linux-gnu/libicuuc.so.67 /usr/lib/$QEMU_TARGET_ARCH-linux-gnu/libicuuc.so.66\ && rm -rf /var/lib/apt/lists/* \ && sed -i 's/^# *\(fr_FR.UTF-8\)/\1/' /etc/locale.gen \ && locale-gen diff --git a/scripts/build/Dockerfile.manylinux b/scripts/build/Dockerfile.manylinux index ab7e05f..8c5cebd 100644 --- a/scripts/build/Dockerfile.manylinux +++ b/scripts/build/Dockerfile.manylinux @@ -8,13 +8,11 @@ FROM $DOCKER_BASE_IMAGE COPY --from=qemu /usr/bin/* /usr/bin/ -#build a new swig -RUN mkdir /build && \ - cd /build && \ - curl -sSfL -O http://prdownloads.sourceforge.net/swig/swig-4.0.1.tar.gz && \ - tar -xzf swig-4.0.1.tar.gz && cd swig-4.0.1 && \ - ./configure --with-python3 && make -j2 && make install && \ - rm -rf /build +# install pip from pypi +RUN pip install swig>=4.2 + +# install setuptools +RUN pip install "setuptools<72" --upgrade # one genicam unittest requires a french locale RUN yum -y reinstall glibc-common @@ -24,4 +22,4 @@ RUN mkdir /pylon_installer RUN mkdir /worker_home && chmod go+rwx /worker_home ENV HOME=/worker_home -RUN uname -a \ No newline at end of file +RUN uname -a diff --git a/scripts/build/build-arch.sh b/scripts/build/build-arch.sh index 803a517..07b1d18 100755 --- a/scripts/build/build-arch.sh +++ b/scripts/build/build-arch.sh @@ -62,9 +62,10 @@ if [ $BUILD_DISTRO = "debian" ]; then cp39) BASE_IMAGE32="python:3.9.16-buster"; BASE_IMAGE64="python:3.9.16-bullseye" ;; cp3_10) BASE_IMAGE32="python:3.10.11-buster"; BASE_IMAGE64="python:3.10.11-bullseye" ;; cp3_11) BASE_IMAGE32="python:3.11.3-buster"; BASE_IMAGE64="python:3.11.3-bullseye" ;; - cp3_12) BASE_IMAGE64="python:3.12.4-bullseye" ;; + cp3_12) BASE_IMAGE64="python:3.12.4-bullseye" ;; + cp3_13) BASE_IMAGE64="python:3.13.0-bullseye" ;; *) - echo "Unsupported abi '$ABI_TAG'. Supported tags: cp36m, cp37m, cp38, cp39, cp3_10, cp3_11, cp3_12" + echo "Unsupported abi '$ABI_TAG'. Supported tags: cp36m, cp37m, cp38, cp39, cp3_10, cp3_11, cp3_12, cp3_13" exit 1 esac else diff --git a/setup.py b/setup.py index 8d2c51a..705eef7 100755 --- a/setup.py +++ b/setup.py @@ -1,8 +1,6 @@ #!/usr/bin/env python -from setuptools import setup, Extension, __version__ as setuptools_version +from setuptools import setup, Extension from setuptools.command.build_ext import new_compiler -from pkg_resources import parse_version -from logging import info, warning, error import argparse import ctypes @@ -16,17 +14,30 @@ import sys import platform from pathlib import Path +from logging import info, warning, error # The pylon version this source tree was designed for, by platform ReferencePylonVersion = { - "Windows": "8.0.0", + "Windows": "9.0.3", # ATTENTION: This version is the pylon core version reported by pylon-config, # which is not equal to the version on the outer tar.gz - "Linux": "8.0.0", + "Linux": "9.0.3", "Linux_armv7l": "6.2.0", "Darwin": "7.3.1", "Darwin_arm64": "7.3.1" } +################################################################################ + +def prepare_for_limited_api(min_ver_str): + min_maj, min_min = map(int, min_ver_str.split(".")) + py_ver_too_low = sys.version_info[:2] < (min_maj, min_min) + # Disable limited api when python version is to low + if py_ver_too_low: + return None, None + return f"0x{min_maj:02x}{min_min:02x}0000", f"cp{min_maj}{min_min}" + +MIN_PY_VER_FOR_LIMITED_API = "3.9" # some low value to prove that it works +LIMIT_DEF, LIMIT_TAG = prepare_for_limited_api(MIN_PY_VER_FOR_LIMITED_API) ################################################################################ @@ -78,7 +89,7 @@ class BuildSupport(object): } [ (get_platform(), get_machinewidth()) ] # Compatible swig versions - SwigVersions = ["4.0.0"] + SwigVersions = ["4.2.0", "4.2.1"] SwigOptions = [ "-c++", "-Wextra", @@ -171,10 +182,10 @@ def is_supported_swig_version(self, swig_executable): if res is None: return False - if tuple(map(int, res.group(1).split('.'))) < (4, 0, 0): + if tuple(map(int, res.group(1).split('.'))) < (4, 2, 0): msg = ( "The version of swig is %s which is too old. " + - "Minimum required version is 4.0.0" + "Minimum required version is 4.2.0" ) warning(msg, res.group(1)) return False @@ -302,14 +313,11 @@ def get_git_version(): warning("git not found or invalid tag found.") warning("-> Building version from date!") now = datetime.datetime.now() - midnight = datetime.datetime(now.year, now.month, now.day) - todays_seconds = (now - midnight).seconds - return "%d.%d.%d.dev%d" % ( + return "%d.%d.%d.dev0" % ( now.year, now.month, - now.day, - todays_seconds - ) + now.day + ) def get_version(self): git_version = self.get_git_version() @@ -391,14 +399,14 @@ def get_pylon_version_tuple(self): # take the leading numerals. parts[3] = re.search(r'\d+', parts[3]).group() return tuple(map(int, parts)) - - + + def include_pylon_data_processing(self): # pylon Versions since 7.0 support data processing but the pypylon mapping has been introduced with 7.4. # previous pylon versions are missing required header files used by pypylon result = self.IncludePylonDataProcessing and self.get_pylon_version_tuple() >= (7, 4, 0, 0) return result - + def get_deploy_list(self): if self.include_pylon_data_processing(): return self.RuntimeDefaultDeploy @@ -459,24 +467,19 @@ class BuildSupportWindows(BuildSupport): } PYLON_DATA_PROCESSING_VTOOLS_DIR = "pylonDataProcessingPlugins" + PYLON_DATA_PROCESSING_VTOOLS_CREATOR_DIR = "DataProcessingPluginsB" RuntimeFolders = { "pylondataprocessing": [ (PYLON_DATA_PROCESSING_VTOOLS_DIR, PYLON_DATA_PROCESSING_VTOOLS_DIR, ("*Editor*.dll",)), + (PYLON_DATA_PROCESSING_VTOOLS_CREATOR_DIR, PYLON_DATA_PROCESSING_VTOOLS_CREATOR_DIR, ()), ], } - # Old versions of distutils use a layman's qouting of commandline - # parameters, that has to be amended with a 'hack'. Newer and fixed - # distutils are used if either (py >= 3.9.0) or (setuptools >= 60.0.0) - correct_qouting = ( - sys.version_info >= (3, 9, 0) or - parse_version(setuptools_version) >= parse_version("60.0.0") - ) - gentl_dir_fmt = r'L"%s\\bin"' if correct_qouting else r'L\"%s\\bin\"' DefineMacros = [ ("UNICODE", None), ("_UNICODE", None), + ("_CRT_SECURE_NO_WARNINGS", None), # let swig share its type information between the 'genicam' and the # 'pylon' module by using the same name for the type table. @@ -767,12 +770,44 @@ class BuildSupportLinux(BuildSupport): (r"libPylonDataProcessingCore\.so\.\d+", ""), ], } - + + # match those shared objects without symlinks directly and where there are + # symlinks, match the first one (*.so.) + RuntimeFiles_starting_9_0_3_215 = { + "base": [ + (r"libpylonbase\.so\.\d+", ""), + ], + "gige": [ + (r"libpylon_TL_gige\.so", ""), + (r"libgxapi\.so\.\d+", "") + ], + "usb": [ + (r"libpylon_TL_usb\.so", ""), + (r"libuxapi\.so\.\d+", ""), + ], + "camemu": [ + (r"libpylon_TL_camemu\.so", "") + ], + "extra": [ + (r"libpylonutility\.so\.\d+", ""), + (r"libpylonutilitypcl\.so\.\d+", ""), + ], + "gentl": [ + (r"libpylon_TL_gtc\.so", ""), + ], + "pylondataprocessing": [ + (r"libPylonDataProcessing\.so\.\d+", ""), + (r"libPylonDataProcessing.sig", ""), + (r"libPylonDataProcessingCore\.so\.\d+", ""), + ], + } PYLON_DATA_PROCESSING_VTOOLS_DIR = "pylondataprocessingplugins" + PYLON_DATA_PROCESSING_VTOOLS_CREATOR_DIR = "dataprocessingpluginsb" RuntimeFolders = { "pylondataprocessing": [ (PYLON_DATA_PROCESSING_VTOOLS_DIR, PYLON_DATA_PROCESSING_VTOOLS_DIR, ("*Editor*.so",)), + (PYLON_DATA_PROCESSING_VTOOLS_CREATOR_DIR, PYLON_DATA_PROCESSING_VTOOLS_CREATOR_DIR, ()), ], } @@ -807,11 +842,14 @@ def __init__(self): print("LibraryDirs:", self.LibraryDirs) # adjust runtime files according to pylon version - olden_days = self.get_pylon_version_tuple() <= (6, 3, 0, 18933) - add_runtime = ( - self.RuntimeFiles_up_to_6_3_0_18933 if olden_days - else self.RuntimeFiles_after_6_3_0_18933 - ) + version = self.get_pylon_version_tuple() + + if version <= (6, 3, 0, 18933): + add_runtime = self.RuntimeFiles_up_to_6_3_0_18933 + elif (6, 3, 0, 18933) < version < (9, 0, 3, 215): + add_runtime = self.RuntimeFiles_after_6_3_0_18933 + else: + add_runtime = self.RuntimeFiles_starting_9_0_3_215 for package in add_runtime: if package in self.RuntimeFiles: self.RuntimeFiles[package].extend(add_runtime[package]) @@ -894,7 +932,13 @@ def call_pylon_dataprocessing_config(self, *args): return res.strip() def get_pylon_version(self): - return self.call_pylon_config("--version") + pylon_version = self.call_pylon_config("--version") + + # workaround for pylon 8.0.0 on linux + if pylon_version == "9...": + pylon_version = "9.0.3.215" + + return pylon_version ################################################################################ @@ -993,7 +1037,13 @@ def call_pylon_config(self, *args): return res.strip() def get_pylon_version(self): - return self.call_pylon_config("--version") + pylon_version = self.call_pylon_config("--version") + + # workaround for pylon 8.0.0 on macOS + if pylon_version == "9...": + pylon_version = "9.0.3.215" + + return pylon_version def get_swig_includes(self): # add compiler include paths to list @@ -1029,7 +1079,7 @@ def copy_runtime(self): if re.match(r".*TL_[a-z]+\.so", p.name): info(f"DELETE {p}") os.remove(p) - + def include_pylon_data_processing(self): return False @@ -1118,6 +1168,9 @@ def include_pylon_data_processing(self): # start with fresh 'pypylon' and 'generated' dirs if not skipping swig bs.clean("skip" if args.skip_swig else "keep") + if LIMIT_DEF: + bs.DefineMacros.append(("Py_LIMITED_API", LIMIT_DEF)) + genicam_wrapper_src = bs.call_swig( "src/genicam", "genicam.i", @@ -1166,6 +1219,7 @@ def include_pylon_data_processing(self): define_macros=bs.DefineMacros, extra_compile_args=bs.ExtraCompileArgs, extra_link_args=bs.ExtraLinkArgs, + py_limited_api=bool(LIMIT_DEF), ) print('\n') pylon_ext = Extension( @@ -1178,6 +1232,7 @@ def include_pylon_data_processing(self): define_macros=bs.DefineMacros, extra_compile_args=bs.ExtraCompileArgs, extra_link_args=bs.ExtraLinkArgs, + py_limited_api=bool(LIMIT_DEF), ) print('\n') if includePylonDataProcessing: @@ -1191,6 +1246,7 @@ def include_pylon_data_processing(self): define_macros=bs.DefineMacros, extra_compile_args=bs.ExtraCompileArgs, extra_link_args=bs.ExtraLinkArgs, + py_limited_api=bool(LIMIT_DEF), ) print('\n') @@ -1224,11 +1280,14 @@ def include_pylon_data_processing(self): "Operating System :: Microsoft :: Windows :: Windows 7", "Operating System :: Microsoft :: Windows :: Windows 8", "Operating System :: Microsoft :: Windows :: Windows 10", + "Operating System :: Microsoft :: Windows :: Windows 11", "Operating System :: POSIX :: Linux", + "Operating System :: MacOS :: MacOS X", "Topic :: Multimedia :: Graphics :: Capture :: Digital Camera", "Topic :: Multimedia :: Video :: Capture", "Topic :: Scientific/Engineering", - ] + ], + options={"bdist_wheel": {"py_limited_api": LIMIT_TAG or False}} ) if args.generate_python_doc: diff --git a/src/genicam/genicam.i b/src/genicam/genicam.i index f68f92c..e32ad5b 100644 --- a/src/genicam/genicam.i +++ b/src/genicam/genicam.i @@ -29,6 +29,21 @@ ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. %include "DoxyGenApi.i"; %begin %{ +#ifdef Py_LIMITED_API +#include // malloc / free +// Although PyMemoryView_FromMemory has been part of limited API since +// version 3.3, the flags PyBUF_READ and PyBUF_WRITE, which are needed to use +// this function, are not defined in newer Python headers unless Py_LIMITED_API +// is set to >= 3.11. Since this is obviously a bug, we need the following +// workarond: +#ifndef PyBUF_READ +#define PyBUF_READ 0x100 +#endif +#ifndef PyBUF_WRITE +#define PyBUF_WRITE 0x200 +#endif +#endif + // allow debug builds of genicam wrapper against release build of python # ifdef _DEBUG # ifdef _MSC_VER @@ -300,16 +315,17 @@ import warnings %typemap(in, noblock=1) (TYPEMAP, SIZE) { %#if PY_VERSION_HEX >= 0x03000000 - Py_buffer buffer_view; int get_buf_res; - get_buf_res = PyObject_GetBuffer($input, &buffer_view, PyBUF_SIMPLE); + Py_ssize_t len; + char *buf; + get_buf_res = PyBytes_AsStringAndSize($input, &buf, &len); if (get_buf_res < 0) { PyErr_Clear(); %argument_fail(get_buf_res, "(TYPEMAP, SIZE)", $symname, $argnum); } - $1 = ($1_ltype) buffer_view.buf; - $2 = ($2_ltype) buffer_view.len; + $1 = ($1_ltype) buf; + $2 = ($2_ltype) len; %#else Py_ssize_t size; const void *buf; @@ -328,9 +344,6 @@ import warnings } %typemap(freearg, noblock=1) (TYPEMAP, SIZE) { -%#if PY_VERSION_HEX >= 0x03000000 - PyBuffer_Release(&buffer_view); -%#endif } %enddef diff --git a/src/pylon/PylonImage.i b/src/pylon/PylonImage.i index 25b62f4..db0a3e3 100644 --- a/src/pylon/PylonImage.i +++ b/src/pylon/PylonImage.i @@ -1,4 +1,5 @@ %rename(PylonImage) Pylon::CPylonImage; +%feature("shadow", "0") Pylon::CPylonImage::AttachMemoryView; %pythoncode %{ from contextlib import contextmanager @@ -28,14 +29,66 @@ (char*)$self->GetBuffer(), $self->GetImageSize(), PyBUF_WRITE - ); + ); %#else PyErr_SetString(PyExc_RuntimeError, "memory view not available"); return NULL; %#endif - } + } + + PyObject* AttachMemoryView(PyObject* object, Pylon::EPixelType pixelType, unsigned int width, unsigned int height, size_t paddingX) { +%#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030b0000 + Py_buffer buffer; + if (PyObject_GetBuffer(memoryView, &buffer, PyBUF_SIMPLE) == -1) { + PyErr_SetString(PyExc_RuntimeError, "Expected a buffer-compatible object"); + Py_RETURN_FALSE; + } + + // Call the existing C++ AttachUserBuffer method + $self->AttachUserBuffer(buffer.buf, buffer.len, pixelType, width, height, paddingX); + + // Release the buffer info + PyBuffer_Release(&buffer); + Py_RETURN_TRUE; +%#else + Py_RETURN_FALSE; +%#endif + } + + PyObject* AttachBytesObject(PyObject* object, Pylon::EPixelType pixelType, unsigned int width, unsigned int height, size_t paddingX) + { + // Check input object is bytes object + if (!PyBytes_Check(object)) { + PyErr_SetString(PyExc_RuntimeError, "Expected a bytes-compatible object"); + Py_RETURN_FALSE; + } + + // Get a pointer to the memory and the buffer size + char* buffer_ptr; + Py_ssize_t buffer_size; + if (PyBytes_AsStringAndSize(object, &buffer_ptr, &buffer_size) != 0) { + PyErr_SetString(PyExc_RuntimeError, "Invalid buffer data"); + Py_RETURN_FALSE; + } + + // Call the existing C++ AttachUserBuffer method + $self->AttachUserBuffer(buffer_ptr, static_cast(buffer_size), pixelType, width, height, paddingX); + + Py_RETURN_TRUE; + } %pythoncode %{ + + def AttachMemoryView(self, memoryView, pixelType, width, height, paddingX): + if memoryView.contiguous == False: + raise ValueError("Expected a memory view with contiguous ordering") + result = _pylon.PylonImage_AttachMemoryView(self, memoryView, pixelType, width, height, paddingX) + if result == False: + memoryViewBuffer = bytes(memoryView) + _pylon.PylonImage_AttachBytesObject(self, memoryViewBuffer, pixelType, width, height, paddingX) + self._memory_view_buffer = memoryViewBuffer # Hold buffer copy to reference to prevent garbage collection + self._memory_view = memoryView # Hold the reference to prevent garbage collection + @needs_numpy def GetImageFormat(self, pt = None): if pt is None: @@ -81,6 +134,13 @@ def __exit__(self, type, value, traceback): self.Release() + @needs_numpy + def AttachArray(self, array, pixeltype): + width = array.shape[1] + height = array.shape[0] + paddingX = 0 # numpy has no concept of padding bytes + self.AttachMemoryView(array.data, pixeltype, width, height, paddingX) + @needs_numpy def GetArray(self, raw = False): @@ -141,5 +201,7 @@ // Ignore original 'GetBuffer' overloads. %ignore GetBuffer; +// Ignore original 'AttachUserBuffer' overloads. +%ignore AttachUserBuffer; %include ; diff --git a/src/pylon/pylon.i b/src/pylon/pylon.i index cc735f2..680182a 100644 --- a/src/pylon/pylon.i +++ b/src/pylon/pylon.i @@ -28,6 +28,22 @@ ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. %module(directors="1", package="pypylon", docstring=PYLON_DOCSTRING) pylon %include "DoxyPylon.i"; %begin %{ + +#ifdef Py_LIMITED_API +#include // malloc / free +// Although PyMemoryView_FromMemory has been part of limited API since +// version 3.3, the flags PyBUF_READ and PyBUF_WRITE, which are needed to use +// this function, are not defined in newer Python headers unless Py_LIMITED_API +// is set to >= 3.11. Since this is obviously a bug, we need the following +// workarond: +#ifndef PyBUF_READ +#define PyBUF_READ 0x100 +#endif +#ifndef PyBUF_WRITE +#define PyBUF_WRITE 0x200 +#endif +#endif + // allow debug builds of genicam wrapper against release build of python # ifdef _DEBUG # ifdef _MSC_VER diff --git a/src/pylondataprocessing/pylondataprocessing.i b/src/pylondataprocessing/pylondataprocessing.i index 7ae1e13..dd9aebc 100644 --- a/src/pylondataprocessing/pylondataprocessing.i +++ b/src/pylondataprocessing/pylondataprocessing.i @@ -28,6 +28,22 @@ ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. %module(directors="1", package="pypylon", docstring=PYLONDP_DOCSTRING) pylondataprocessing %include "DoxyPylonDataProcessing.i"; %begin %{ + +#ifdef Py_LIMITED_API +#include // malloc / free +// Although PyMemoryView_FromMemory has been part of limited API since +// version 3.3, the flags PyBUF_READ and PyBUF_WRITE, which are needed to use +// this function, are not defined in newer Python headers unless Py_LIMITED_API +// is set to >= 3.11. Since this is obviously a bug, we need the following +// workarond: +#ifndef PyBUF_READ +#define PyBUF_READ 0x100 +#endif +#ifndef PyBUF_WRITE +#define PyBUF_WRITE 0x200 +#endif +#endif + // allow debug builds of genicam wrapper against release build of python # ifdef _DEBUG # ifdef _MSC_VER diff --git a/tests/pylon_tests/emulated/pylonimage_test.py b/tests/pylon_tests/emulated/pylonimage_test.py index 19230d4..c73c530 100644 --- a/tests/pylon_tests/emulated/pylonimage_test.py +++ b/tests/pylon_tests/emulated/pylonimage_test.py @@ -70,6 +70,27 @@ def test_container_load_zero_copy(self): self.assertEqual(zc[0,0], 143) testee.Release() + def test_attacharray(self): + import numpy as np + import sys + + img = pylon.PylonImage() + arr = np.random.randint(0, 256, (480, 640), dtype=np.uint8) + + arr_refcount_0 = sys.getrefcount(arr) + img.AttachArray(arr, pylon.PixelType_Mono8) + # check proper refcounting attach + self.assertEqual( sys.getrefcount(arr) , arr_refcount_0 + 1) + + # check that img and array have same content + self.assertEqual(np.all(arr == img.Array), True) + + del img + # check proper refcounting free + self.assertEqual( sys.getrefcount(arr), arr_refcount_0) + + + if __name__ == "__main__": # import sys;sys.argv = ['', 'Test.testName'] unittest.main()