From 0f1cfe630b45b88ed20a2db1212f7e9b3ee2ca18 Mon Sep 17 00:00:00 2001
From: Syping <schiedelrafael@keppe.org>
Date: Thu, 9 Nov 2023 20:17:37 +0100
Subject: [PATCH] libragephoto: add ragephoto Python Package

- separate RagePhoto and RagePhoto-Extract sources
---
 CMakeLists.txt                            | 106 ++++++----
 README.md                                 |   8 +-
 cmake/wasm.cmake                          |   2 +-
 doc/Doxyfile.in                           |   2 +-
 src/{ => core}/RagePhoto                  |   0
 src/{ => core}/RagePhoto.c                |   0
 src/{ => core}/RagePhoto.cpp              |   8 +-
 src/{ => core}/RagePhoto.h                |   0
 src/{ => core}/RagePhoto.hpp              |   0
 src/{ => core}/RagePhotoA                 |   0
 src/{ => core}/RagePhotoA.hpp             |   0
 src/{ => core}/RagePhotoB                 |   0
 src/{ => core}/RagePhotoB.hpp             |   0
 src/{ => core}/RagePhotoConfig.h.in       |   0
 src/{ => core}/RagePhotoLibrary.h         |   0
 src/{ => core}/RagePhotoTypedefs.h        |   0
 src/{ => core}/ragephoto.pc.in            |   0
 src/{ => core}/ragephoto.rc.in            |   0
 src/{ => extract}/RagePhoto-Extract.c     |   0
 src/{ => extract}/RagePhoto-Extract.cpp   |   0
 src/{ => extract}/ragephoto-extract.rc.in |   0
 src/python/__init__.py                    |  25 +++
 src/python/__version__.py.in              |  20 ++
 src/python/libragephoto.py                |  88 ++++++++
 src/python/pyproject.toml.in              |  18 ++
 src/python/ragephoto.py                   | 243 ++++++++++++++++++++++
 src/python/setup.py.in                    |  33 +++
 27 files changed, 503 insertions(+), 50 deletions(-)
 rename src/{ => core}/RagePhoto (100%)
 rename src/{ => core}/RagePhoto.c (100%)
 rename src/{ => core}/RagePhoto.cpp (99%)
 rename src/{ => core}/RagePhoto.h (100%)
 rename src/{ => core}/RagePhoto.hpp (100%)
 rename src/{ => core}/RagePhotoA (100%)
 rename src/{ => core}/RagePhotoA.hpp (100%)
 rename src/{ => core}/RagePhotoB (100%)
 rename src/{ => core}/RagePhotoB.hpp (100%)
 rename src/{ => core}/RagePhotoConfig.h.in (100%)
 rename src/{ => core}/RagePhotoLibrary.h (100%)
 rename src/{ => core}/RagePhotoTypedefs.h (100%)
 rename src/{ => core}/ragephoto.pc.in (100%)
 rename src/{ => core}/ragephoto.rc.in (100%)
 rename src/{ => extract}/RagePhoto-Extract.c (100%)
 rename src/{ => extract}/RagePhoto-Extract.cpp (100%)
 rename src/{ => extract}/ragephoto-extract.rc.in (100%)
 create mode 100644 src/python/__init__.py
 create mode 100644 src/python/__version__.py.in
 create mode 100644 src/python/libragephoto.py
 create mode 100644 src/python/pyproject.toml.in
 create mode 100644 src/python/ragephoto.py
 create mode 100644 src/python/setup.py.in

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 9e3cdd1..30d842d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -24,7 +24,7 @@ include(GNUInstallDirs)
 include(cmake/cxxstd.cmake)
 include(cmake/unicode.cmake)
 
-# RagePhoto Top Level ON
+# RagePhoto Top Level
 if (${CMAKE_PROJECT_NAME} STREQUAL "ragephoto")
     set(RPTL_ON ON)
 else()
@@ -35,59 +35,53 @@ endif()
 option(RAGEPHOTO_C_LIBRARY "Build libragephoto as C library" OFF)
 if (RAGEPHOTO_C_LIBRARY)
     set(RAGEPHOTO_HEADERS
-        src/RagePhoto.h
-        src/RagePhotoA
-        src/RagePhotoA.hpp
-        src/RagePhotoB
-        src/RagePhotoB.hpp
-        src/RagePhotoLibrary.h
-        src/RagePhotoTypedefs.h
+        src/core/RagePhoto.h
+        src/core/RagePhotoA
+        src/core/RagePhotoA.hpp
+        src/core/RagePhotoB
+        src/core/RagePhotoB.hpp
+        src/core/RagePhotoLibrary.h
+        src/core/RagePhotoTypedefs.h
     )
     set(RAGEPHOTO_SOURCES
-        src/RagePhoto.c
+        src/core/RagePhoto.c
     )
 else()
     set(RAGEPHOTO_HEADERS
-        src/RagePhoto
-        src/RagePhoto.hpp
-        src/RagePhotoB
-        src/RagePhotoB.hpp
-        src/RagePhotoLibrary.h
-        src/RagePhotoTypedefs.h
+        src/core/RagePhoto
+        src/core/RagePhoto.hpp
+        src/core/RagePhotoB
+        src/core/RagePhotoB.hpp
+        src/core/RagePhotoLibrary.h
+        src/core/RagePhotoTypedefs.h
     )
     set(RAGEPHOTO_SOURCES
-        src/RagePhoto.cpp
+        src/core/RagePhoto.cpp
     )
 endif()
 
 # RagePhoto Library Type
 option(RAGEPHOTO_STATIC "Build libragephoto as static library" OFF)
 if (RAGEPHOTO_STATIC)
-    option(RAGEPHOTO_C_API "Build libragephoto with C API support" OFF)
     set(LIBRAGEPHOTO_LIBTYPE LIBRAGEPHOTO_STATIC)
 else()
-    option(RAGEPHOTO_C_API "Build libragephoto with C API support" ON)
     set(LIBRAGEPHOTO_LIBTYPE LIBRAGEPHOTO_SHARED)
 endif()
 
 # RagePhoto Benchmark
 option(RAGEPHOTO_BENCHMARK "Build with libragephoto benchmark (C++ only)" OFF)
-if (RAGEPHOTO_BENCHMARK)
-    list(APPEND LIBRAGEPHOTO_DEFINES
-        RAGEPHOTO_BENCHMARK
-    )
-endif()
 
-# RagePhoto C API
+# RagePhoto API
+option(RAGEPHOTO_C_API "Build libragephoto with C API support" ON)
 if (RAGEPHOTO_C_LIBRARY)
     set(LIBRAGEPHOTO_API LIBRAGEPHOTO_C_ONLY)
 else()
     if (RAGEPHOTO_C_API)
         set(LIBRAGEPHOTO_API LIBRAGEPHOTO_CXX_C)
         list(APPEND RAGEPHOTO_HEADERS
-            src/RagePhoto.h
-            src/RagePhotoA
-            src/RagePhotoA.hpp
+            src/core/RagePhoto.h
+            src/core/RagePhotoA
+            src/core/RagePhotoA.hpp
         )
     else()
         set(LIBRAGEPHOTO_API LIBRAGEPHOTO_CXX_ONLY)
@@ -97,15 +91,15 @@ endif()
 # RagePhoto Win32 Shared Resources
 if (WIN32)
     string(TIMESTAMP ragephoto_BUILD_YEAR "%Y" UTC)
-    configure_file(src/ragephoto.rc.in "${ragephoto_BINARY_DIR}/resources/ragephoto.rc" @ONLY)
+    configure_file(src/core/ragephoto.rc.in "${ragephoto_BINARY_DIR}/resources/ragephoto.rc" @ONLY)
     list(APPEND RAGEPHOTO_SHARED_RESOURCES
         "${ragephoto_BINARY_DIR}/resources/ragephoto.rc"
     )
 endif()
 
 # RagePhoto Configures + Target + Installs
-configure_file(src/ragephoto.pc.in "${ragephoto_BINARY_DIR}/pkgconfig/ragephoto.pc" @ONLY)
-configure_file(src/RagePhotoConfig.h.in "${ragephoto_BINARY_DIR}/include/RagePhotoConfig.h" @ONLY)
+configure_file(src/core/ragephoto.pc.in "${ragephoto_BINARY_DIR}/pkgconfig/ragephoto.pc" @ONLY)
+configure_file(src/core/RagePhotoConfig.h.in "${ragephoto_BINARY_DIR}/include/RagePhotoConfig.h" @ONLY)
 list(APPEND RAGEPHOTO_HEADERS
     "${ragephoto_BINARY_DIR}/include/RagePhotoConfig.h"
 )
@@ -122,13 +116,14 @@ endif()
 target_compile_definitions(ragephoto PRIVATE
     LIBRAGEPHOTO_LIBRARY
     ${LIBRAGEPHOTO_DEFINES}
+    $<$<BOOL:${RAGEPHOTO_BENCHMARK}>:RAGEPHOTO_BENCHMARK>
 )
-if (MSVC AND MSVC_VERSION GREATER_EQUAL 1914 AND NOT RAGEPHOTO_C_LIBRARY)
-    target_compile_options(ragephoto PRIVATE "/Zc:__cplusplus")
+if (MSVC AND MSVC_VERSION GREATER_EQUAL 1914)
+    target_compile_options(ragephoto PRIVATE $<$<COMPILE_LANGUAGE:CXX>:/Zc:__cplusplus>)
 endif()
 target_include_directories(ragephoto PUBLIC
     "${ragephoto_BINARY_DIR}/include"
-    "${ragephoto_SOURCE_DIR}/src"
+    "${ragephoto_SOURCE_DIR}/src/core"
 )
 install(TARGETS ragephoto
     ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}"
@@ -167,14 +162,14 @@ option(RAGEPHOTO_EXTRACT "Build libragephoto with ragephoto-extract" ${RPTL_ON})
 if (RAGEPHOTO_EXTRACT)
     # RagePhoto-Extract Source files
     if (RAGEPHOTO_C_API)
-        set(EXTRACT_SOURCES src/RagePhoto-Extract.c)
+        set(EXTRACT_SOURCES src/extract/RagePhoto-Extract.c)
     else()
-        set(EXTRACT_SOURCES src/RagePhoto-Extract.cpp)
+        set(EXTRACT_SOURCES src/extract/RagePhoto-Extract.cpp)
     endif()
     # RagePhoto-Extract Win32 Shared Resources
     if (WIN32)
         string(TIMESTAMP ragephoto_BUILD_YEAR "%Y" UTC)
-        configure_file(src/ragephoto-extract.rc.in "${ragephoto_BINARY_DIR}/resources/ragephoto-extract.rc" @ONLY)
+        configure_file(src/extract/ragephoto-extract.rc.in "${ragephoto_BINARY_DIR}/resources/ragephoto-extract.rc" @ONLY)
         list(APPEND EXTRACT_RESOURCES
             "${ragephoto_BINARY_DIR}/resources/ragephoto-extract.rc"
         )
@@ -184,13 +179,48 @@ if (RAGEPHOTO_EXTRACT)
     set_target_properties(ragephoto-extract PROPERTIES
         INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}"
     )
-    if (MSVC AND MSVC_VERSION GREATER_EQUAL 1914 AND NOT RAGEPHOTO_C_API)
-        target_compile_options(ragephoto-extract PRIVATE "/Zc:__cplusplus")
+    if (MSVC AND MSVC_VERSION GREATER_EQUAL 1914)
+        target_compile_options(ragephoto-extract PRIVATE $<$<COMPILE_LANGUAGE:CXX>:/Zc:__cplusplus>)
     endif()
     target_link_libraries(ragephoto-extract PRIVATE ragephoto)
     install(TARGETS ragephoto-extract DESTINATION "${CMAKE_INSTALL_BINDIR}")
 endif()
 
+# RagePhoto Python Package
+option(RAGEPHOTO_PYTHON "Create ragephoto Python Package" OFF)
+if (RAGEPHOTO_PYTHON)
+    # Python Package Library file
+    if (WIN32)
+        set(PYRAGEPHOTO_LIBRARY "libragephoto.dll")
+    else()
+        set(PYRAGEPHOTO_LIBRARY "libragephoto.so")
+    endif()
+    # Generate Python Package Project files
+    configure_file(src/python/setup.py.in "${ragephoto_BINARY_DIR}/pyragephoto/setup.py" @ONLY)
+    configure_file(src/python/pyproject.toml.in "${ragephoto_BINARY_DIR}/pyragephoto/pyproject.toml" @ONLY)
+    configure_file(src/python/__version__.py.in "${ragephoto_BINARY_DIR}/pyragephoto/ragephoto/__version__.py" @ONLY)
+    # Python Package Source files + Target
+    set(PYRAGEPHOTO_SOURCES
+        "src/python/__init__.py"
+        "src/python/libragephoto.py"
+        "src/python/ragephoto.py"
+    )
+    add_custom_target(pyragephoto SOURCES ${PYRAGEPHOTO_SOURCES})
+    # Copy Python Package to build directory
+    file(COPY ${PYRAGEPHOTO_SOURCES} DESTINATION "${ragephoto_BINARY_DIR}/pyragephoto/ragephoto")
+    # Python Package Bundle Settings
+    option(RAGEPHOTO_PYTHON_BUNDLE_LIBRARY "Bundle libragephoto with ragephoto Python Package" OFF)
+    if (RAGEPHOTO_PYTHON_BUNDLE_LIBRARY)
+        add_custom_command(
+            TARGET ragephoto
+            POST_BUILD
+            COMMAND "${CMAKE_COMMAND}" -E copy "$<TARGET_FILE:ragephoto>" "${ragephoto_BINARY_DIR}/pyragephoto/ragephoto/${PYRAGEPHOTO_LIBRARY}"
+            BYPRODUCTS "${ragephoto_BINARY_DIR}/pyragephoto/ragephoto/${PYRAGEPHOTO_LIBRARY}"
+            VERBATIM
+        )
+    endif()
+endif()
+
 # CPack Package Generation
 if (RPTL_ON)
     include(InstallRequiredSystemLibraries)
diff --git a/README.md b/README.md
index f439b17..7858009 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,10 @@
 ## libragephoto
 Open Source RAGE Photo Parser for GTA V and RDR 2
 
-- Read/Write RAGE Photos error free and correct  
-- Support for metadata stored in RAGE Photos  
-- Export RAGE Photos to jpeg with ragephoto-extract  
-- High Efficient and Simple C/C++ API
+  - Read/Write RAGE Photos error free and correct
+  - Support for metadata stored in RAGE Photos
+  - Export RAGE Photos to jpeg with ragephoto-extract
+  - High Efficient and Simple C/C++ API
 
 #### Build libragephoto
 
diff --git a/cmake/wasm.cmake b/cmake/wasm.cmake
index 5da54dd..ad3e1d6 100644
--- a/cmake/wasm.cmake
+++ b/cmake/wasm.cmake
@@ -40,7 +40,7 @@ if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.13.0")
     )
     target_include_directories(ragephoto-wasm PUBLIC
         "${ragephoto_BINARY_DIR}/include"
-        "${ragephoto_SOURCE_DIR}/src"
+        "${ragephoto_SOURCE_DIR}/src/core"
     )
 else()
     message(WARNING "A useable WebAssembly build needs at least CMake 3.13.0 or newer")
diff --git a/doc/Doxyfile.in b/doc/Doxyfile.in
index c846f9e..2e37135 100644
--- a/doc/Doxyfile.in
+++ b/doc/Doxyfile.in
@@ -3,7 +3,7 @@ PROJECT_NUMBER         = "Version: @ragephoto_VERSION@"
 INPUT                  = "@CMAKE_CURRENT_SOURCE_DIR@/index.doc" \
                          "@CMAKE_CURRENT_SOURCE_DIR@/build.doc" \
                          "@CMAKE_CURRENT_SOURCE_DIR@/usage.doc" \
-                         "src"
+                         "src/core"
 OUTPUT_DIRECTORY       = "@CMAKE_CURRENT_BINARY_DIR@"
 EXTRACT_PRIVATE        = NO
 ENABLE_PREPROCESSING   = YES
diff --git a/src/RagePhoto b/src/core/RagePhoto
similarity index 100%
rename from src/RagePhoto
rename to src/core/RagePhoto
diff --git a/src/RagePhoto.c b/src/core/RagePhoto.c
similarity index 100%
rename from src/RagePhoto.c
rename to src/core/RagePhoto.c
diff --git a/src/RagePhoto.cpp b/src/core/RagePhoto.cpp
similarity index 99%
rename from src/RagePhoto.cpp
rename to src/core/RagePhoto.cpp
index 0bf72ed..8f17faf 100644
--- a/src/RagePhoto.cpp
+++ b/src/core/RagePhoto.cpp
@@ -157,16 +157,12 @@ inline uint32_t joaatFromInitial(const char *data, size_t size, uint32_t init_va
 RagePhoto::RagePhoto()
 {
     m_data = static_cast<RagePhotoData*>(malloc(sizeof(RagePhotoData)));
-    if (!m_data) {
+    if (!m_data)
         throw std::runtime_error("RagePhotoData data struct can't be allocated");
-        return;
-    }
     memset(m_data, 0, sizeof(RagePhotoData));
     m_parser = static_cast<RagePhotoFormatParser*>(malloc(sizeof(RagePhotoFormatParser)));
-    if (!m_parser) {
+    if (!m_parser)
         throw std::runtime_error("RagePhotoFormatParser parser struct can't be allocated");
-        return;
-    }
     memset(m_parser, 0, sizeof(RagePhotoFormatParser));
     setBufferDefault(m_data);
 }
diff --git a/src/RagePhoto.h b/src/core/RagePhoto.h
similarity index 100%
rename from src/RagePhoto.h
rename to src/core/RagePhoto.h
diff --git a/src/RagePhoto.hpp b/src/core/RagePhoto.hpp
similarity index 100%
rename from src/RagePhoto.hpp
rename to src/core/RagePhoto.hpp
diff --git a/src/RagePhotoA b/src/core/RagePhotoA
similarity index 100%
rename from src/RagePhotoA
rename to src/core/RagePhotoA
diff --git a/src/RagePhotoA.hpp b/src/core/RagePhotoA.hpp
similarity index 100%
rename from src/RagePhotoA.hpp
rename to src/core/RagePhotoA.hpp
diff --git a/src/RagePhotoB b/src/core/RagePhotoB
similarity index 100%
rename from src/RagePhotoB
rename to src/core/RagePhotoB
diff --git a/src/RagePhotoB.hpp b/src/core/RagePhotoB.hpp
similarity index 100%
rename from src/RagePhotoB.hpp
rename to src/core/RagePhotoB.hpp
diff --git a/src/RagePhotoConfig.h.in b/src/core/RagePhotoConfig.h.in
similarity index 100%
rename from src/RagePhotoConfig.h.in
rename to src/core/RagePhotoConfig.h.in
diff --git a/src/RagePhotoLibrary.h b/src/core/RagePhotoLibrary.h
similarity index 100%
rename from src/RagePhotoLibrary.h
rename to src/core/RagePhotoLibrary.h
diff --git a/src/RagePhotoTypedefs.h b/src/core/RagePhotoTypedefs.h
similarity index 100%
rename from src/RagePhotoTypedefs.h
rename to src/core/RagePhotoTypedefs.h
diff --git a/src/ragephoto.pc.in b/src/core/ragephoto.pc.in
similarity index 100%
rename from src/ragephoto.pc.in
rename to src/core/ragephoto.pc.in
diff --git a/src/ragephoto.rc.in b/src/core/ragephoto.rc.in
similarity index 100%
rename from src/ragephoto.rc.in
rename to src/core/ragephoto.rc.in
diff --git a/src/RagePhoto-Extract.c b/src/extract/RagePhoto-Extract.c
similarity index 100%
rename from src/RagePhoto-Extract.c
rename to src/extract/RagePhoto-Extract.c
diff --git a/src/RagePhoto-Extract.cpp b/src/extract/RagePhoto-Extract.cpp
similarity index 100%
rename from src/RagePhoto-Extract.cpp
rename to src/extract/RagePhoto-Extract.cpp
diff --git a/src/ragephoto-extract.rc.in b/src/extract/ragephoto-extract.rc.in
similarity index 100%
rename from src/ragephoto-extract.rc.in
rename to src/extract/ragephoto-extract.rc.in
diff --git a/src/python/__init__.py b/src/python/__init__.py
new file mode 100644
index 0000000..b01d1a8
--- /dev/null
+++ b/src/python/__init__.py
@@ -0,0 +1,25 @@
+##############################################################################
+# libragephoto for Python
+# Copyright (C) 2023 Syping
+#
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# This software is provided as-is, no warranties are given to you, we are not
+# responsible for anything with use of the software, you are self responsible.
+##############################################################################
+
+from .ragephoto import RagePhoto
+
+__all__ = [
+    "libragephoto", # libragephoto Loader Module
+    "ragephoto", # RagePhoto Module
+    "RagePhoto" # RagePhoto API
+]
diff --git a/src/python/__version__.py.in b/src/python/__version__.py.in
new file mode 100644
index 0000000..0168254
--- /dev/null
+++ b/src/python/__version__.py.in
@@ -0,0 +1,20 @@
+##############################################################################
+# libragephoto for Python
+# Copyright (C) 2023 Syping
+#
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# This software is provided as-is, no warranties are given to you, we are not
+# responsible for anything with use of the software, you are self responsible.
+##############################################################################
+
+VERSION = (@ragephoto_VERSION_MAJOR@, @ragephoto_VERSION_MINOR@, @ragephoto_VERSION_PATCH@)
+__version__ = "@ragephoto_VERSION@"
diff --git a/src/python/libragephoto.py b/src/python/libragephoto.py
new file mode 100644
index 0000000..e129293
--- /dev/null
+++ b/src/python/libragephoto.py
@@ -0,0 +1,88 @@
+##############################################################################
+# libragephoto for Python
+# Copyright (C) 2023 Syping
+#
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# This software is provided as-is, no warranties are given to you, we are not
+# responsible for anything with use of the software, you are self responsible.
+##############################################################################
+
+from ctypes import *
+from ctypes.util import find_library
+from pathlib import Path
+from platform import system
+
+if system() == "Windows":
+  bundle_library_path = Path(__file__).parent.resolve() / "libragephoto.dll"
+  if bundle_library_path.is_file():
+    library_path = str(bundle_library_path)
+else:
+  bundle_library_path = Path(__file__).parent.resolve() / "libragephoto.so"
+  if bundle_library_path.is_file():
+    library_path = str(bundle_library_path)
+  else:
+    library_path = find_library("ragephoto")
+
+if not library_path:
+  raise ImportError("libragephoto is required.")
+
+libragephoto = cdll.LoadLibrary(library_path)
+libragephoto.ragephoto_open.restype = c_void_p
+libragephoto.ragephoto_clear.argtypes = [c_void_p]
+libragephoto.ragephoto_close.argtypes = [c_void_p]
+libragephoto.ragephoto_load.argtypes = [c_void_p, POINTER(c_char), c_size_t]
+libragephoto.ragephoto_load.restype = c_bool
+libragephoto.ragephoto_loadfile.argtypes = [c_void_p, c_char_p]
+libragephoto.ragephoto_loadfile.restype = c_bool
+libragephoto.ragephoto_error.argtypes = [c_void_p]
+libragephoto.ragephoto_error.restype = c_int32
+libragephoto.ragephoto_getphotodesc.argtypes = [c_void_p]
+libragephoto.ragephoto_getphotodesc.restype = c_char_p
+libragephoto.ragephoto_getphotoformat.argtypes = [c_void_p]
+libragephoto.ragephoto_getphotoformat.restype = c_uint32
+libragephoto.ragephoto_getphotojpeg.argtypes = [c_void_p]
+libragephoto.ragephoto_getphotojpeg.restype = POINTER(c_char)
+libragephoto.ragephoto_getphotojson.argtypes = [c_void_p]
+libragephoto.ragephoto_getphotojson.restype = c_char_p
+libragephoto.ragephoto_getphotoheader.argtypes = [c_void_p]
+libragephoto.ragephoto_getphotoheader.restype = c_char_p
+libragephoto.ragephoto_getphotosign.argtypes = [c_void_p]
+libragephoto.ragephoto_getphotosign.restype = c_uint64
+libragephoto.ragephoto_getphotosignf.argtypes = [c_void_p, c_uint32]
+libragephoto.ragephoto_getphotosignf.restype = c_uint64
+libragephoto.ragephoto_getphotosize.argtypes = [c_void_p]
+libragephoto.ragephoto_getphotosize.restype = c_uint32
+libragephoto.ragephoto_getphototitle.argtypes = [c_void_p]
+libragephoto.ragephoto_getphototitle.restype = c_char_p
+libragephoto.ragephoto_getsavesize.argtypes = [c_void_p]
+libragephoto.ragephoto_getsavesize.restype = c_size_t
+libragephoto.ragephoto_getsavesizef.argtypes = [c_void_p, c_uint32]
+libragephoto.ragephoto_getsavesizef.restype = c_size_t
+libragephoto.ragephoto_save.argtypes = [c_void_p, POINTER(c_char)]
+libragephoto.ragephoto_save.restype = c_bool
+libragephoto.ragephoto_savef.argtypes = [c_void_p, POINTER(c_char), c_uint32]
+libragephoto.ragephoto_savef.restype = c_bool
+libragephoto.ragephoto_savefile.argtypes = [c_void_p, c_char_p]
+libragephoto.ragephoto_savefile.restype = c_bool
+libragephoto.ragephoto_savefilef.argtypes = [c_void_p, c_char_p, c_uint32]
+libragephoto.ragephoto_savefilef.restype = c_bool
+libragephoto.ragephoto_setbufferdefault.argtypes = [c_void_p]
+libragephoto.ragephoto_setbufferoffsets.argtypes = [c_void_p]
+libragephoto.ragephoto_setphotodesc.argtypes = [c_void_p, c_char_p, c_uint32]
+libragephoto.ragephoto_setphotoformat.argtypes = [c_void_p, c_uint32]
+libragephoto.ragephoto_setphotojpeg.argtypes = [c_void_p, POINTER(c_char), c_uint32, c_uint32]
+libragephoto.ragephoto_setphotojpeg.restype = c_bool
+libragephoto.ragephoto_setphotojson.argtypes = [c_void_p, c_char_p, c_uint32]
+libragephoto.ragephoto_setphotoheader.argtypes = [c_void_p, c_char_p, c_uint32]
+libragephoto.ragephoto_setphotoheader2.argtypes = [c_void_p, c_char_p, c_uint32, c_uint32]
+libragephoto.ragephoto_setphototitle.argtypes = [c_void_p, c_char_p, c_uint32]
+libragephoto.ragephoto_version.restype = c_char_p
diff --git a/src/python/pyproject.toml.in b/src/python/pyproject.toml.in
new file mode 100644
index 0000000..97a882e
--- /dev/null
+++ b/src/python/pyproject.toml.in
@@ -0,0 +1,18 @@
+[project]
+name = "ragephoto"
+version = "@ragephoto_VERSION@"
+authors = [
+  { name = "Syping" },
+]
+description = "libragephoto for Python"
+requires-python = ">=3.6"
+classifiers = [
+  "License :: OSI Approved :: BSD-2-Clause",
+]
+
+[build-system]
+requires = ["setuptools", "wheel"]
+build-backend = "setuptools.build_meta"
+
+[project.urls]
+"Homepage" = "https://libragephoto.syping.de/"
diff --git a/src/python/ragephoto.py b/src/python/ragephoto.py
new file mode 100644
index 0000000..0ceaaa5
--- /dev/null
+++ b/src/python/ragephoto.py
@@ -0,0 +1,243 @@
+##############################################################################
+# libragephoto for Python
+# Copyright (C) 2023 Syping
+#
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# This software is provided as-is, no warranties are given to you, we are not
+# responsible for anything with use of the software, you are self responsible.
+##############################################################################
+
+from .libragephoto import *
+from enum import IntEnum
+from json import loads as parseJson
+from json import dumps as serializeJson
+
+class RagePhoto:
+  class DefaultSize(IntEnum):
+    DEFAULT_GTA5_PHOTOBUFFER = 524288
+    DEFAULT_RDR2_PHOTOBUFFER = 1048576
+    DEFAULT_DESCBUFFER = 256
+    DEFAULT_JSONBUFFER = 3072
+    DEFAULT_TITLBUFFER = 256
+
+  class Error(IntEnum):
+    DescBufferTight = 39
+    DescMallocError = 31
+    DescReadError = 32
+    HeaderBufferTight = 35
+    HeaderMallocError = 4
+    IncompatibleFormat = 2
+    IncompleteChecksum = 7
+    IncompleteDescBuffer = 30
+    IncompleteDescMarker = 28
+    IncompleteDescOffset = 11
+    IncompleteEOF = 8
+    IncompleteHeader = 3
+    IncompleteJendMarker = 33
+    IncompleteJpegMarker = 12
+    IncompleteJsonBuffer = 20
+    IncompleteJsonMarker = 18
+    IncompleteJsonOffset = 9
+    IncompletePhotoBuffer = 14
+    IncompletePhotoSize = 15
+    IncompleteTitleBuffer = 25
+    IncompleteTitleMarker = 23
+    IncompleteTitleOffset = 10
+    IncorrectDescMarker = 29
+    IncorrectJendMarker = 34
+    IncorrectJpegMarker = 13
+    IncorrectJsonMarker = 19
+    IncorrectTitleMarker = 24
+    JsonBufferTight = 37
+    JsonMallocError = 21
+    JsonReadError = 22
+    NoError = 255
+    NoFormatIdentifier = 1
+    PhotoBufferTight = 36
+    PhotoMallocError = 16
+    PhotoReadError = 17
+    TitleBufferTight = 38
+    TitleMallocError = 26
+    TitleReadError = 27
+    UnicodeInitError = 5
+    UnicodeHeaderError = 6
+    Uninitialised = 0
+
+  class PhotoFormat(IntEnum):
+    GTA5 = 0x01000000
+    RDR2 = 0x04000000
+
+  def __init__(self):
+    self.__instance = libragephoto.ragephoto_open()
+
+  def __del__(self):
+    libragephoto.ragephoto_close(self.__instance)
+
+  def clear(self):
+    libragephoto.ragephoto_clear(self.__instance)
+
+  def load(self, data):
+    return libragephoto.ragephoto_load(self.__instance, data, len(data))
+
+  def loadFile(self, file):
+    if isinstance(file, str):
+      return libragephoto.ragephoto_loadfile(self.__instance, file.encode())
+    else:
+      return libragephoto.ragephoto_loadfile(self.__instance, file)
+
+  def error(self):
+    return libragephoto.ragephoto_error(self.__instance)
+
+  def description(self):
+    _desc = libragephoto.ragephoto_getphotodesc(self.__instance)
+    if _desc:
+      return _desc
+    else:
+      return b""
+
+  def format(self):
+    return libragephoto.ragephoto_getphotoformat(self.__instance)
+
+  def jpeg(self):
+    _jpeg = libragephoto.ragephoto_getphotojpeg(self.__instance)
+    if _jpeg:
+      return _jpeg[:self.jpegSize()]
+    else:
+      return b""
+
+  def jpegSign(self, format = None):
+    if format is None:
+      return libragephoto.ragephoto_getphotosign(self.__instance)
+    else:
+      return libragephoto.ragephoto_getphotosignf(self.__instance, format)
+
+  def jpegSize(self):
+    return libragephoto.ragephoto_getphotosize(self.__instance)
+
+  def json(self):
+    _json = libragephoto.ragephoto_getphotojson(self.__instance)
+    if _json:
+      return _json
+    else:
+      return b""
+
+  def header(self):
+    _header = libragephoto.ragephoto_getphotoheader(self.__instance)
+    if _header:
+      return _header
+    else:
+      return b""
+
+  def save(self, format = None):
+    _data = bytearray(self.saveSize(format))
+    _ptr = (c_char * len(_data)).from_buffer(_data)
+    if format is None:
+      _ret = libragephoto.ragephoto_save(self.__instance, _ptr)
+    else:
+      _ret = libragephoto.ragephoto_savef(self.__instance, _ptr, format)
+    if _ret:
+      return _data
+    else:
+      return None
+
+  def saveFile(self, file, format = None):
+    if isinstance(file, str):
+      _file = file.encode()
+    else:
+      _file = file
+    if format is None:
+      return libragephoto.ragephoto_savefile(self.__instance, _file)
+    else:
+      return libragephoto.ragephoto_savefilef(self.__instance, _file, format)
+
+  def saveSize(self, format = None):
+    if format is None:
+      return libragephoto.ragephoto_getsavesize(self.__instance)
+    else:
+      return libragephoto.ragephoto_getsavesizef(self.__instance, format)
+
+  def setBufferDefault(self):
+    return libragephoto.ragephoto_setbufferdefault(self.__instance)
+
+  def setBufferOffsets(self):
+    return libragephoto.ragephoto_setbufferoffsets(self.__instance)
+
+  def setDescription(self, desc, buffer = None):
+    if isinstance(desc, str):
+      _desc = desc.encode()
+    else:
+      _desc = desc
+    if buffer is None:
+      libragephoto.ragephoto_setphotodesc(self.__instance, _desc, self.DefaultSize.DEFAULT_DESCBUFFER)
+    else:
+      libragephoto.ragephoto_setphotodesc(self.__instance, _desc, buffer)
+
+  def setFormat(self, format):
+    libragephoto.ragephoto_setphotoformat(self.__instance, format)
+
+  def setJpeg(self, jpeg, buffer = None):
+    _buffer = 0
+    if buffer is None:
+      _format = self.format()
+      if _format == self.PhotoFormat.GTA5:
+        _buffer = self.DefaultSize.DEFAULT_GTA5_PHOTOBUFFER
+      elif _format == self.PhotoFormat.RDR2:
+        _buffer = self.DefaultSize.DEFAULT_RDR2_PHOTOBUFFER
+    if _buffer < len(jpeg):
+      _buffer = len(jpeg)
+    return libragephoto.ragephoto_setphotojpeg(self.__instance, jpeg, len(jpeg), _buffer)
+
+  def setJson(self, json, buffer = None):
+    if isinstance(json, str):
+      _json = json.encode()
+    else:
+      _json = json
+    if buffer is None:
+      libragephoto.ragephoto_setphotojson(self.__instance, _json, self.DefaultSize.DEFAULT_JSONBUFFER)
+    else:
+      libragephoto.ragephoto_setphotojson(self.__instance, _json, buffer)
+
+  def setHeader(self, header, headerSum1, headerSum2 = 0):
+    if isinstance(header, str):
+      _header = header.encode()
+    else:
+      _header = header
+    libragephoto.ragephoto_setphotoheader2(self.__instance, _header, headerSum1, headerSum2)
+
+  def setTitle(self, title, buffer = None):
+    if isinstance(title, str):
+      _title = title.encode()
+    else:
+      _title = title
+    if buffer is None:
+      libragephoto.ragephoto_setphototitle(self.__instance, _title, self.DefaultSize.DEFAULT_TITLBUFFER)
+    else:
+      libragephoto.ragephoto_setphototitle(self.__instance, _title, buffer)
+
+  def title(self):
+    _title = libragephoto.ragephoto_getphototitle(self.__instance)
+    if _title:
+      return _title
+    else:
+      return b""
+
+  def updateSign(self):
+    try:
+      _json = parseJson(self.json())
+    except JSONDecodeError:
+      return False
+    _json["sign"] = self.jpegSign()
+    self.setJson(serializeJson(_json, separators=(',', ':')))
+    return True
+
+  def version(self):
+    return libragephoto.ragephoto_version()
diff --git a/src/python/setup.py.in b/src/python/setup.py.in
new file mode 100644
index 0000000..b996135
--- /dev/null
+++ b/src/python/setup.py.in
@@ -0,0 +1,33 @@
+#!/usr/bin/env python3
+
+##############################################################################
+# libragephoto for Python
+# Copyright (C) 2023 Syping
+#
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# This software is provided as-is, no warranties are given to you, we are not
+# responsible for anything with use of the software, you are self responsible.
+##############################################################################
+
+from setuptools import setup, find_packages
+
+setup(
+    name = "ragephoto",
+    version = "@ragephoto_VERSION@",
+    author = "Syping",
+    packages = ["ragephoto"],
+    url = "https://libragephoto.syping.de/",
+    description = "libragephoto for Python",
+    classifiers = [
+        "License :: OSI Approved :: BSD-2-Clause",
+    ]
+)