├── .gitignore ├── CMakeLists.txt ├── LICENSE ├── README.md ├── data └── locale │ └── en-US.ini └── src ├── config.h.in └── obs-ghostscript.c /.gitignore: -------------------------------------------------------------------------------- 1 | #binaries 2 | *.exe 3 | *.dll 4 | *.dylib 5 | *.so 6 | 7 | #cmake 8 | /cmbuild/ 9 | /build/ 10 | /build32/ 11 | /build64/ 12 | /release/ 13 | /release32/ 14 | /release64/ 15 | /debug/ 16 | /debug32/ 17 | /debug64/ 18 | /builds/ 19 | *.o.d 20 | *.ninja 21 | .ninja* 22 | .dirstamp 23 | 24 | #xcode 25 | *.xcodeproj/ 26 | 27 | #other stuff (windows stuff, qt moc stuff, etc) 28 | Release_MD/ 29 | Release/ 30 | Debug/ 31 | x64/ 32 | ipch/ 33 | GeneratedFiles/ 34 | .moc/ 35 | 36 | /other/ 37 | 38 | #make stuff 39 | configure 40 | depcomp 41 | install-sh 42 | Makefile.in 43 | Makefile 44 | 45 | #random useless file stuff 46 | *.dmg 47 | *.app 48 | .DS_Store 49 | .directory 50 | .hg 51 | .depend 52 | tags 53 | *.trace 54 | *.vsp 55 | *.psess 56 | *.swp 57 | *.dat 58 | *.clbin 59 | *.log 60 | *.tlog 61 | *.sdf 62 | *.opensdf 63 | *.xml 64 | *.ipch 65 | *.css 66 | *.xslt 67 | *.aps 68 | *.suo 69 | *.ncb 70 | *.user 71 | *.lo 72 | *.ilk 73 | *.la 74 | *.o 75 | *.obj 76 | *.pdb 77 | *.res 78 | *.manifest 79 | *.dep 80 | *.zip 81 | *.lnk 82 | *.chm 83 | *~ 84 | .DS_Store 85 | */.DS_Store 86 | */**/.DS_Store 87 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required (VERSION 2.8.12) 2 | 3 | foreach(flag_var 4 | CMAKE_C_FLAGS CMAKE_C_FLAGS_DEBUG CMAKE_C_FLAGS_RELEASE 5 | CMAKE_C_FLAGS_MINSIZEREL CMAKE_C_FLAGS_RELWITHDEBINFO) 6 | if(${flag_var} MATCHES "/MD") 7 | string(REGEX REPLACE "/MD" "/MT" ${flag_var} "${${flag_var}}") 8 | endif(${flag_var} MATCHES "/MD") 9 | endforeach(flag_var) 10 | 11 | project (obs-ghostscript) 12 | 13 | include(CheckCSourceCompiles) 14 | 15 | if (OBSSourcePath OR DEFINED ENV{OBSSourcePath}) 16 | # Set already 17 | else() 18 | set(OBSSourcePath "" CACHE PATH "Path to OBS source code (e.g., C:/Dev/obs-studio/libobs/)") 19 | message("OBSSourcePath is missing. Please set this variable to the location of the OBS source (e.g., C:/Dev/obs-studio/libobs/).") 20 | endif() 21 | 22 | if (NOT OBSLibaryPath AND NOT DEFINED ENV{OBSLibraryPath}) 23 | set (OBSLibraryPath "" CACHE PATH "Path to OBS libraries (e.g., C:/Dev/obs-studio/build64/libobs/Release/)") 24 | endif() 25 | 26 | if (NOT GSSourcePath AND NOT DEFINED ENV{GSSourcePath}) 27 | set (GSSourcePath "" CACHE PATH "Path to Ghostscript source code (e.g., C:/Dev/gnu-ghostscript-9.21/)") 28 | endif() 29 | 30 | if (NOT GSLibraryPath AND NOT DEFINED ENV{GSLibraryPath}) 31 | set (GSLibraryPath "" CACHE PATH "Path to Ghostscript libraries (e.g., C:/Program Files/gs/gs9.21/bin)") 32 | endif() 33 | 34 | if(CMAKE_SIZEOF_VOID_P EQUAL 8) 35 | set(_lib_suffix 64) 36 | else() 37 | set(_lib_suffix 32) 38 | endif() 39 | 40 | find_library(OBS_LIB_PATH 41 | NAMES obs 42 | HINTS 43 | ${OBSLibraryPath}) 44 | 45 | find_path(GHOSTSCRIPT_INCLUDE_DIR 46 | NAMES ghostscript/iapi.h ghostscript/gdevdsp.h 47 | HINTS ${GSSourcePath}) 48 | 49 | find_path(GHOSTSCRIPT_SOURCE_DIR 50 | NAMES psi/iapi.h devices/gdevdsp.h 51 | HINTS ${GSSourcePath}) 52 | 53 | if (GHOSTSCRIPT_SOURCE_DIR AND NOT GHOSTSCRIPT_INCLUDE_DIR) 54 | set(GHOSTSCRIPT_INCLUDE_DIR ${GHOSTSCRIPT_SOURCE_DIR}) 55 | set(GHOSTSCRIPT_IN_SOURCE_TREE 1) 56 | else() 57 | set(GHOSTSCRIPT_IN_SOURCE_TREE 0) 58 | endif() 59 | 60 | find_library(GHOSTSCRIPT_LIB_PATH 61 | NAMES libgs.so gsdll${_lib_suffix}.lib 62 | HINTS 63 | ${GSLibraryPath}) 64 | 65 | if (NOT GHOSTSCRIPT_INCLUDE_DIR OR NOT GHOSTSCRIPT_LIB_PATH) 66 | message("Ghostscript headers or libraries could not be found! Please ensure that Ghostscript is installed somewhere, and set the GSSourcePath and GSLibraryPath variables if necessary.") 67 | endif() 68 | 69 | configure_file(${CMAKE_SOURCE_DIR}/src/config.h.in ${CMAKE_BINARY_DIR}/config/config.h) 70 | 71 | # Source 72 | file (GLOB SOURCES ${CMAKE_SOURCE_DIR}/src/*.c) 73 | file (GLOB HEADER_FILES ${CMAKE_SOURCE_DIR}/include/*.h ${CMAKE_BINARY_DIR}/config/*.h) 74 | 75 | include_directories (include ${CMAKE_BINARY_DIR}/config) 76 | add_library (${PROJECT_NAME} SHARED 77 | ${SOURCES} 78 | ${HEADER_FILES} 79 | ) 80 | 81 | # libobs 82 | include_directories(${OBSSourcePath}) 83 | add_library (libobs SHARED IMPORTED) 84 | if (WIN32) 85 | get_filename_component(_obs_lib_dir ${OBS_LIB_PATH} DIRECTORY) 86 | 87 | set_property (TARGET libobs PROPERTY IMPORTED_IMPLIB ${OBS_LIB_PATH}) 88 | set_property (TARGET libobs PROPERTY IMPORTED_LOCATION ${_obs_lib_dir}/obs.dll) 89 | else() 90 | set_property (TARGET libobs PROPERTY IMPORTED_LOCATION ${OBS_LIB_PATH}) 91 | endif() 92 | target_link_libraries (${PROJECT_NAME} libobs) 93 | 94 | # Ghostscript 95 | include_directories(${GHOSTSCRIPT_INCLUDE_DIR}) 96 | add_library (libgs SHARED IMPORTED) 97 | if (WIN32) 98 | get_filename_component(_gs_lib_dir ${GHOSTSCRIPT_LIB_PATH} DIRECTORY) 99 | 100 | set_property (TARGET libgs PROPERTY IMPORTED_IMPLIB ${GHOSTSCRIPT_LIB_PATH}) 101 | set_property (TARGET libgs PROPERTY IMPORTED_LOCATION ${_gs_lib_dir}/gsdll${_lib_suffix}.dll) 102 | else() 103 | set_property (TARGET libgs PROPERTY IMPORTED_LOCATION ${GHOSTSCRIPT_LIB_PATH}) 104 | endif() 105 | target_link_libraries (${PROJECT_NAME} libgs) 106 | 107 | if (GHOSTSCRIPT_IN_SOURCE_TREE EQUAL 1) 108 | add_definitions(/DGS_IN_SOURCE_TREE) 109 | endif() 110 | 111 | 112 | if (WIN32) 113 | install(TARGETS ${PROJECT_NAME} RUNTIME DESTINATION obs-plugins/${_lib_suffix}bit) 114 | install(FILES ${CMAKE_BINARY_DIR}/Debug/${PROJECT_NAME}.pdb DESTINATION obs-plugins/${_lib_suffix}bit CONFIGURATIONS Debug) 115 | install(FILES ${_gs_lib_dir}/gsdll${_lib_suffix}.dll DESTINATION obs-plugins/${_lib_suffix}bit) 116 | else() 117 | install(TARGETS ${PROJECT_NAME} LIBRARY DESTINATION obs-plugins/${_lib_suffix}bit) 118 | endif() 119 | install(DIRECTORY data/ DESTINATION data/obs-plugins/${PROJECT_NAME}/) 120 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # obs-ghostscript 2 | 3 | ## Introduction 4 | 5 | The obs-ghostscript plugin is intended to allow an easy way to include a specific PDF document (or other type 6 | supported by the [Ghostscript](https://ghostscript.com/) library) in your [OBS Studio](https://obsproject.com/) 7 | scenes. It should be a simpler alternative to using a window capture source to show the contents of an Acrobat 8 | window. 9 | 10 | The document is shown one page at a time, and the current page can be updated real-time through an interaction 11 | window to scroll through a document live during a capture session. 12 | 13 | ## Installation 14 | 15 | The binary package mirrors the structure of the OBS Studio installation directory, so you should be able to 16 | just drop its contents alongside an OBS Studio install (usually at C:\Program Files (x86)\obs-studio\). The 17 | necessary files should look like this: 18 | 19 | obs-studio 20 | |---data 21 | | |---obs-plugins 22 | | |---obs-ghostscript 23 | | |---locale 24 | | |---en-US.ini 25 | |---obs-plugins 26 | |---32bit 27 | | |---gsdll32.dll 28 | | |---obs-ghostscript.dll 29 | |---64bit 30 | |---gsdll64.dll 31 | |---obs-ghostscript.dll 32 | 33 | Note that binary packages are currently only available for Windows; I'm really not sure how reliable a Linux 34 | binary package would be on the wide variety of systems out there. 35 | 36 | ## Usage 37 | 38 | The source is called "PDF Document (Ghostscript)" in OBS Studio's source creation menu. The properties to set 39 | on it are the path to the document file and the page from the document which should be shown. 40 | 41 | You can also enter the document password for password-protected PDFs. **OBS Studio will store this setting in 42 | plain text** in its local data files; if you're deeply concerned about someone breaking into your computer and 43 | stealing your datas, you might not want to use this option. 44 | 45 | Hotkeys are provided to modify the displayed page more quickly while recording/streaming than the source 46 | properties dialog allows. These can be set through the main Hotkeys tab in the OBS Studio settings. 47 | 48 | You can also manipulate the current page by right-clicking on the source and choosing "Interact". 49 | With the source displayed in an interaction window, you can scroll your mouse wheel and change the 50 | displayed page, or use the arrow keys or PgUp/PgDown keys. (Note that the source will only receive mouse 51 | events if the mouse is hovered over the image shown in the intraction window.) 52 | 53 | If the native size of the document is not appropriate for your use case, you can specify settings which 54 | will control the size at which it will be rendered. Selecting "Override DPI" and changing the "DPI" option 55 | will change the DPI (dots per inch) used when rendering. Increasing this will increase the size in pixels 56 | of the document proportionally. 57 | 58 | You can also specify the page size Ghostscript uses when interpreting the document by selecting "Override Page 59 | Size" and specifying a width and height. These values are in points, not pixels; 1 point is 1/72 inch. The 60 | conversion from points to rendered pixels is dependent on the DPI setting, either that contained in the document 61 | or the one specified in the source properties. You can choose whether Ghostscript will scale the contents of the 62 | document to fit this page size or not as well, but that option is specific to PDF documents. 63 | 64 | ## Building 65 | 66 | If you wish to build the obs-ghostscript plugin from source, you should just need [CMake](https://cmake.org/), 67 | the OBS Studio libraries and headers, and the Ghostscript libraries and headers. 68 | 69 | * [obs-ghostscript source repository](https://github.com/nleseul/obs-ghostscript) 70 | * [OBS studio source repository](https://github.com/jp9000/obs-studio) 71 | * [Ghostscript source repository](http://git.ghostscript.com/?p=ghostpdl.git;a=summary) 72 | 73 | I don't believe that the OBS project provides prebuilt libraries; you're probably going to have the best luck 74 | building your own OBS binaries from the source. Refer to the OBS repository for more information on that. 75 | 76 | The [standard Ghostscript installers](https://www.ghostscript.com/download/gsdnld.html) do provide binary 77 | libraries for linking, but they don't provide the Ghostscript header files. On Windows, you may have the 78 | best luck downloading the binary release for the library, and then harvesting the headers from the source 79 | release. Linux environments should be able to install a libgs-dev package or something similar to acquire 80 | both libraries and headers. 81 | 82 | You might also be okay just harvesting the necessary headers from Ghostscript's web documentation; 83 | obs-ghostscript only uses two headers—[psi/api.h](https://www.ghostscript.com/doc/psi/iapi.h) and 84 | [devices/gdevdsp.h](https://www.ghostscript.com/doc/devices/gdevdsp.h). If you download those individually, 85 | ensure that they are arranged in the proper folders. The Linux dev package would probably place them 86 | directly in a `ghostscript` subfolder of a system include directory (e.g., /usr/include/ghostscript); the 87 | CMake script should detect that arrangement of headers as well. 88 | 89 | ### Windows 90 | 91 | When building in CMake, you will probably need to set four configuration values so your environment can be 92 | properly set up: 93 | 94 | * `OBSSourcePath` should refer to the libobs subfolder in the OBS source release. 95 | * `OBSLibraryPath` should refer to the folder where the libobs library binaries are. 96 | * `GSSourcePath` should refer to the root folder of a Ghostscript source distribution (or a subset thereof 97 | containing the two aforementioned headers). 98 | * `GSLibraryPath` should refer to a folder containing the gsdll[32|64].lib binary libraries. 99 | 100 | Installation logic is provided through CMake as well; you can set the `CMAKE_INSTALL_PREFIX` configuration value 101 | to choose the folder to which the files will be copied. You can also manually copy all files to the locations 102 | described above. 103 | 104 | ### Linux 105 | 106 | Note that I've only tested this plugin briefly in Linux via an Ubuntu VM; I don't know how well it will work 107 | in other distros, or what the performance is like on a real machine. 108 | 109 | As with Windows, you will need to specify the `OBSSourcePath` and `OBSLibraryPath` variables on the CMake 110 | command line. A Linux environment will probably have Ghostscript available in a standard system location, 111 | if you've installed it through a package like libgs-dev in your package manager, so it shouldn't be necessary 112 | to manually specify the paths to Ghostscript. 113 | 114 | In my particular case, the build process looked like this, starting from the obs-ghostscript directory: 115 | 116 | mkdir build64 117 | cd build64 118 | cmake .. -DOBSSourcePath=~/Development/External/obs-studio/libobs/ -DOBSLibraryPath=~/obs-studio/bin/64bit/ -DCMAKE_INSTALL_PREFIX=~/obs-studio 119 | make 120 | make install 121 | 122 | Your Linux may vary. 123 | 124 | ## License 125 | 126 | This project is licensed under the "[Unlicense](http://unlicense.org/)", because copy[right|left] is a hideous 127 | mess to deal with and I don't like it. 128 | 129 | The Ghostscript binaries included in binary distributions of this project are copyright [Artifex Software, 130 | Inc.](https://www.ghostscript.com/Licensing.html) and licensed under the GNU Affero General 131 | Public License (AGPL). The source code is available from locations previously listed in this document. 132 | Refer to the Ghostscript project's [licensing information](https://www.ghostscript.com/Licensing.html) 133 | for more details. 134 | -------------------------------------------------------------------------------- /data/locale/en-US.ini: -------------------------------------------------------------------------------- 1 | PdfSource="PDF Document (Ghostscript)" 2 | PdfSource.FileName="Source File" 3 | PdfSource.PageNumber="Page Number" 4 | PdfSource.PrevPage="Previous Page" 5 | PdfSource.NextPage="Next Page" 6 | PdfSource.Password="PDF Password" 7 | PdfSource.ShouldOverridePageSize="Override Page Size" 8 | PdfSource.OverridePageSize.Width="Page Width (points)" 9 | PdfSource.OverridePageSize.Height="Page Height (points)" 10 | PdfSource.OverridePageSize.FitToPage="Fit Content to Page (PDF only)" 11 | PdfSource.ShouldOverrideDpi="Override DPI" 12 | PdfSource.OverrideDpi="DPI" 13 | -------------------------------------------------------------------------------- /src/config.h.in: -------------------------------------------------------------------------------- 1 | #pragma once 2 | -------------------------------------------------------------------------------- /src/obs-ghostscript.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | // Ghostscript expects a non-standard #define for indicating a Windows environment, apparently. 11 | // This ensures the Ghostscript API functions are referenced by the linker with the correct 12 | // calling convention (on x86). 13 | #ifdef _WIN32 14 | #define _WINDOWS_ 15 | #endif 16 | 17 | #ifdef GS_IN_SOURCE_TREE 18 | #include 19 | #include 20 | #else 21 | #include 22 | #include 23 | #endif 24 | 25 | #include 26 | 27 | static void *shared_ghostscript_instance = NULL; 28 | 29 | struct pdf_source { 30 | 31 | char *file_path; 32 | unsigned int page_number; 33 | 34 | uint32_t width; 35 | uint32_t height; 36 | gs_texture_t *texture; 37 | 38 | bool should_override_page_size; 39 | int override_width; 40 | int override_height; 41 | bool override_fit_to_page; 42 | 43 | bool should_override_dpi; 44 | int override_dpi; 45 | 46 | int cached_rasterwidth; 47 | int cached_rasterheight; 48 | int cached_rasterrowsizeinbytes; 49 | unsigned char *cached_raster; 50 | bool cached_anypagesrendered; 51 | 52 | char *password; 53 | 54 | obs_hotkey_pair_id change_page_hotkey_pair; 55 | 56 | obs_source_t *src; 57 | }; 58 | 59 | static int ghostscript_display_open(void *handle, void *device) 60 | { 61 | return 0; 62 | } 63 | 64 | static int ghostscript_display_preclose(void *handle, void *device) 65 | { 66 | return 0; 67 | } 68 | 69 | static int ghostscript_display_close(void *handle, void *device) 70 | { 71 | return 0; 72 | } 73 | 74 | static int ghostscript_display_presize(void *handle, void *device, int width, int height, int raster, unsigned int format) 75 | { 76 | return 0; 77 | } 78 | 79 | static int ghostscript_display_size(void *handle, void *device, int width, int height, int raster, unsigned int format, unsigned char *pimage) 80 | { 81 | struct pdf_source *context = handle; 82 | 83 | context->cached_rasterwidth = width; 84 | context->cached_rasterheight = height; 85 | context->cached_rasterrowsizeinbytes = raster; 86 | context->cached_raster = pimage; 87 | 88 | return 0; 89 | } 90 | 91 | static int ghostscript_display_sync(void *handle, void *device) 92 | { 93 | return 0; 94 | } 95 | 96 | static int ghostscript_display_page(void *handle, void *device, int copies, int flush) 97 | { 98 | struct pdf_source *context = handle; 99 | 100 | obs_enter_graphics(); 101 | 102 | if (context->texture != NULL && (context->width != context->cached_rasterwidth || context->height != context->cached_rasterheight)) 103 | { 104 | gs_texture_destroy(context->texture); 105 | context->texture = NULL; 106 | } 107 | 108 | context->cached_anypagesrendered = true; 109 | context->width = context->cached_rasterwidth; 110 | context->height = context->cached_rasterheight; 111 | 112 | if (context->texture == NULL) 113 | { 114 | context->texture = gs_texture_create(context->cached_rasterrowsizeinbytes / 4, context->cached_rasterheight, 115 | GS_BGRX, 1, &context->cached_raster, GS_DYNAMIC); 116 | } 117 | else 118 | { 119 | gs_texture_set_image(context->texture, context->cached_raster, context->cached_rasterrowsizeinbytes, false); 120 | } 121 | 122 | obs_leave_graphics(); 123 | 124 | return 0; 125 | } 126 | 127 | static int ghostscript_display_update(void *handle, void *device, 128 | int x, int y, int w, int h) 129 | { 130 | return 0; 131 | } 132 | 133 | display_callback display = 134 | { 135 | sizeof(display_callback), 136 | DISPLAY_VERSION_MAJOR, 137 | DISPLAY_VERSION_MINOR, 138 | ghostscript_display_open, 139 | ghostscript_display_preclose, 140 | ghostscript_display_close, 141 | ghostscript_display_presize, 142 | ghostscript_display_size, 143 | ghostscript_display_sync, 144 | ghostscript_display_page, 145 | ghostscript_display_update, 146 | NULL, 147 | NULL, 148 | NULL 149 | }; 150 | 151 | 152 | static void pdf_source_load(struct pdf_source *context) 153 | { 154 | DARRAY(char *) arguments; 155 | 156 | context->cached_anypagesrendered = false; 157 | 158 | if (context->file_path != NULL) 159 | { 160 | char *command_ignored = "gs"; 161 | char *device_type = "-sDEVICE=display"; 162 | char *no_pause = "-dNOPAUSE"; 163 | struct dstr display_format_buffer = { 0 }; 164 | struct dstr display_handle_buffer = { 0 }; 165 | struct dstr page_list_buffer = { 0 }; 166 | char *file_flag = "-f"; 167 | 168 | dstr_printf(&display_format_buffer, "-dDisplayFormat=%d", DISPLAY_COLORS_RGB | DISPLAY_UNUSED_LAST | 169 | DISPLAY_DEPTH_8 | DISPLAY_LITTLEENDIAN | DISPLAY_TOPFIRST); 170 | dstr_printf(&display_handle_buffer, "-sDisplayHandle=16#%"PRIx64"", (uint64_t)context); 171 | dstr_printf(&page_list_buffer, "-sPageList=%d", context->page_number); 172 | 173 | da_init(arguments); 174 | 175 | da_push_back(arguments, &command_ignored); 176 | da_push_back(arguments, &device_type); 177 | da_push_back(arguments, &no_pause); 178 | da_push_back(arguments, &display_handle_buffer.array); 179 | da_push_back(arguments, &display_format_buffer.array); 180 | da_push_back(arguments, &page_list_buffer.array); 181 | 182 | if (context->password != NULL && strcmp(context->password, "") != 0) 183 | { 184 | struct dstr password_buffer = { 0 }; 185 | dstr_printf(&password_buffer, "-sPDFPassword=%s", context->password); 186 | 187 | da_push_back(arguments, &password_buffer.array); 188 | } 189 | 190 | if (context->should_override_page_size) 191 | { 192 | char *fixed_media = "-dFIXEDMEDIA"; 193 | struct dstr width_buffer = { 0 }; 194 | struct dstr height_buffer = { 0 }; 195 | 196 | dstr_printf(&width_buffer, "-dDEVICEWIDTHPOINTS=%d", context->override_width); 197 | dstr_printf(&height_buffer, "-dDEVICEHEIGHTPOINTS=%d", context->override_height); 198 | 199 | da_push_back(arguments, &fixed_media); 200 | 201 | if (context->override_fit_to_page) 202 | { 203 | char *fit_page = "-dPDFFitPage"; 204 | da_push_back(arguments, &fit_page); 205 | } 206 | 207 | da_push_back(arguments, &width_buffer.array); 208 | da_push_back(arguments, &height_buffer.array); 209 | } 210 | 211 | if (context->should_override_dpi) 212 | { 213 | struct dstr dpi_buffer = { 0 }; 214 | 215 | dstr_printf(&dpi_buffer, "-r%d", context->override_dpi); 216 | da_push_back(arguments, &dpi_buffer.array); 217 | } 218 | 219 | da_push_back(arguments, &file_flag); 220 | da_push_back(arguments, &context->file_path); 221 | 222 | // Here, we execute the Ghostscript command to parse the file and render the document. The display device 223 | // callbacks indicated above will handle copying the Ghostscript buffer into an OBS texture if any page is 224 | // rendered. 225 | gsapi_init_with_args(shared_ghostscript_instance, (int)arguments.num, arguments.array); 226 | gsapi_exit(shared_ghostscript_instance); 227 | 228 | da_free(arguments); 229 | } 230 | 231 | if (!context->cached_anypagesrendered && context->texture != NULL) 232 | { 233 | // If the ghostscript_display_page() callback above was never called, that means the commands issued 234 | // to Ghostscript did not result in a page in the document being rendered. That is most likely to happen 235 | // if the document does not contain the requested page number. We simply destroy the texture and render 236 | // nothing in that case. 237 | obs_enter_graphics(); 238 | gs_texture_destroy(context->texture); 239 | context->texture = NULL; 240 | obs_leave_graphics(); 241 | } 242 | } 243 | 244 | 245 | static void pdf_source_change_page(struct pdf_source *context, 246 | bool change_to_previous) 247 | { 248 | if (change_to_previous && context->page_number > 1) 249 | { 250 | context->page_number--; 251 | } 252 | else if (!change_to_previous && context->page_number < 9999) 253 | { 254 | context->page_number++; 255 | } 256 | 257 | pdf_source_load(context); 258 | } 259 | 260 | 261 | static bool pdf_source_hotkey_prev(void *data, obs_hotkey_pair_id id, 262 | obs_hotkey_t *hotkey, bool pressed) 263 | { 264 | if (pressed) 265 | { 266 | pdf_source_change_page(data, true); 267 | } 268 | 269 | return true; 270 | } 271 | 272 | static bool pdf_source_hotkey_next(void *data, obs_hotkey_pair_id id, 273 | obs_hotkey_t *hotkey, bool pressed) 274 | { 275 | if (pressed) 276 | { 277 | pdf_source_change_page(data, false); 278 | } 279 | 280 | return true; 281 | } 282 | 283 | static bool pdf_source_override_size_changed(obs_properties_t *props, 284 | obs_property_t *property, obs_data_t *settings) 285 | { 286 | bool should_override_size = obs_data_get_bool(settings, "should_override_page_size"); 287 | 288 | obs_property_set_visible(obs_properties_get(props, "override_width"), should_override_size); 289 | obs_property_set_visible(obs_properties_get(props, "override_height"), should_override_size); 290 | obs_property_set_visible(obs_properties_get(props, "override_fit_to_page"), should_override_size); 291 | 292 | return true; 293 | } 294 | 295 | static bool pdf_source_override_dpi_changed(obs_properties_t *props, 296 | obs_property_t *property, obs_data_t *settings) 297 | { 298 | bool should_override_dpi = obs_data_get_bool(settings, "should_override_dpi"); 299 | 300 | obs_property_set_visible(obs_properties_get(props, "override_dpi"), should_override_dpi); 301 | 302 | return true; 303 | } 304 | 305 | 306 | 307 | static const char *pdf_source_get_name(void *unused) 308 | { 309 | UNUSED_PARAMETER(unused); 310 | return obs_module_text("PdfSource"); 311 | } 312 | 313 | static void pdf_source_update(void *data, obs_data_t *settings) 314 | { 315 | struct pdf_source *context = data; 316 | const char *file_path = obs_data_get_string(settings, "file_path"); 317 | const char *password = obs_data_get_string(settings, "password"); 318 | 319 | if (context->file_path != NULL) 320 | { 321 | bfree(context->file_path); 322 | } 323 | context->file_path = bstrdup(file_path); 324 | context->page_number = (unsigned int)obs_data_get_int(settings, "page_number"); 325 | 326 | if (context->password != NULL) 327 | { 328 | bfree(context->password); 329 | } 330 | context->password = bstrdup(password); 331 | 332 | context->should_override_page_size = obs_data_get_bool(settings, "should_override_page_size"); 333 | context->override_width = (int)obs_data_get_int(settings, "override_width"); 334 | context->override_height = (int)obs_data_get_int(settings, "override_height"); 335 | context->override_fit_to_page = obs_data_get_bool(settings, "override_fit_to_page"); 336 | 337 | context->should_override_dpi = obs_data_get_bool(settings, "should_override_dpi"); 338 | context->override_dpi = (int)obs_data_get_int(settings, "override_dpi"); 339 | 340 | pdf_source_load(context); 341 | } 342 | 343 | static void *pdf_source_create(obs_data_t *settings, obs_source_t *source) 344 | { 345 | struct pdf_source *context = bzalloc(sizeof(struct pdf_source)); 346 | context->src = source; 347 | 348 | context->change_page_hotkey_pair = obs_hotkey_pair_register_source(source, 349 | "PdfSource.PrevPage", obs_module_text("PdfSource.PrevPage"), 350 | "PdfSource.NextPage", obs_module_text("PdfSource.NextPage"), 351 | pdf_source_hotkey_prev, pdf_source_hotkey_next, context, context); 352 | 353 | pdf_source_update(context, settings); 354 | 355 | return context; 356 | } 357 | 358 | static void pdf_source_destroy(void *data) 359 | { 360 | struct pdf_source *context = data; 361 | 362 | if (context->file_path != NULL) 363 | { 364 | bfree(context->file_path); 365 | context->file_path = NULL; 366 | } 367 | 368 | if (context->password != NULL) 369 | { 370 | bfree(context->password); 371 | context->password = NULL; 372 | } 373 | 374 | if (context->texture != NULL) 375 | { 376 | gs_texture_destroy(context->texture); 377 | context->texture = NULL; 378 | } 379 | 380 | obs_hotkey_pair_unregister(context->change_page_hotkey_pair); 381 | context->change_page_hotkey_pair = OBS_INVALID_HOTKEY_PAIR_ID; 382 | 383 | bfree(context); 384 | } 385 | 386 | static const char *file_type_filter = "Ghostscript document files (*.pdf *.ps *.eps *.epsf);;"; 387 | 388 | 389 | static obs_properties_t *pdf_source_properties(void *data) 390 | { 391 | struct pdf_source *context = data; 392 | 393 | obs_properties_t *props = obs_properties_create(); 394 | struct dstr path = { 0 }; 395 | 396 | if (context && context->file_path != NULL && context->file_path[0] != 0) 397 | { 398 | const char *slash; 399 | 400 | dstr_copy(&path, context->file_path); 401 | dstr_replace(&path, "\\", "/"); 402 | slash = strrchr(path.array, '/'); 403 | if (slash) 404 | dstr_resize(&path, slash - path.array + 1); 405 | } 406 | 407 | obs_properties_add_path(props, "file_path", 408 | obs_module_text("PdfSource.FileName"), OBS_PATH_FILE, file_type_filter, path.array); 409 | 410 | obs_properties_add_int(props, "page_number", obs_module_text("PdfSource.PageNumber"), 1, 9999, 1); 411 | 412 | obs_properties_add_text(props, "password", obs_module_text("PdfSource.Password"), OBS_TEXT_PASSWORD); 413 | 414 | obs_property_t *should_override_dpi_prop = obs_properties_add_bool(props, "should_override_dpi", 415 | obs_module_text("PdfSource.ShouldOverrideDpi")); 416 | obs_properties_add_int(props, "override_dpi", obs_module_text("PdfSource.OverrideDpi"), 1, 600, 1); 417 | 418 | obs_property_t *should_override_page_size_prop = obs_properties_add_bool(props, "should_override_page_size", 419 | obs_module_text("PdfSource.ShouldOverridePageSize")); 420 | obs_properties_add_int(props, "override_width", obs_module_text("PdfSource.OverridePageSize.Width"), 1, INT_MAX, 1); 421 | obs_properties_add_int(props, "override_height", obs_module_text("PdfSource.OverridePageSize.Height"), 1, INT_MAX, 1); 422 | obs_properties_add_bool(props, "override_fit_to_page", obs_module_text("PdfSource.OverridePageSize.FitToPage")); 423 | 424 | obs_property_set_modified_callback(should_override_page_size_prop, pdf_source_override_size_changed); 425 | obs_property_set_modified_callback(should_override_dpi_prop, pdf_source_override_dpi_changed); 426 | 427 | return props; 428 | } 429 | 430 | static void pdf_source_render(void *data, gs_effect_t *effect) 431 | { 432 | UNUSED_PARAMETER(effect); 433 | 434 | struct pdf_source *context = data; 435 | 436 | if (context->texture != NULL) 437 | { 438 | gs_effect_set_texture(gs_effect_get_param_by_name(effect, "image"), 439 | context->texture); 440 | gs_draw_sprite(context->texture, 0, 441 | context->width, context->height); 442 | } 443 | } 444 | 445 | static void pdf_source_key_click(void *data, 446 | const struct obs_key_event *event, bool key_up) 447 | { 448 | struct pdf_source *context = data; 449 | 450 | if (!key_up) 451 | { 452 | obs_key_t key = obs_key_from_virtual_key(event->native_vkey); 453 | 454 | switch (key) 455 | { 456 | case OBS_KEY_UP: 457 | case OBS_KEY_PAGEUP: 458 | pdf_source_change_page(context, true); 459 | break; 460 | 461 | case OBS_KEY_DOWN: 462 | case OBS_KEY_PAGEDOWN: 463 | pdf_source_change_page(context, false); 464 | break; 465 | } 466 | } 467 | } 468 | 469 | static void pdf_source_mouse_wheel(void *data, 470 | const struct obs_mouse_event *event, int x_delta, int y_delta) 471 | { 472 | struct pdf_source *context = data; 473 | 474 | if (y_delta > 0) 475 | { 476 | pdf_source_change_page(context, true); 477 | } 478 | else if (y_delta < 0) 479 | { 480 | pdf_source_change_page(context, false); 481 | } 482 | } 483 | 484 | static uint32_t pdf_source_getwidth(void *data) 485 | { 486 | struct pdf_source *context = data; 487 | return context->width; 488 | } 489 | 490 | static uint32_t pdf_source_getheight(void *data) 491 | { 492 | struct pdf_source *context = data; 493 | return context->height; 494 | } 495 | 496 | static void pdf_source_defaults(obs_data_t *settings) 497 | { 498 | obs_data_set_default_int(settings, "page_number", 1); 499 | obs_data_set_default_bool(settings, "should_override_page_size", false); 500 | obs_data_set_default_bool(settings, "override_fit_to_page", true); 501 | obs_data_set_default_bool(settings, "should_override_dpi", false); 502 | obs_data_set_default_int(settings, "override_dpi", 72); 503 | } 504 | 505 | struct obs_source_info pdf_source_info = { 506 | .id = "obs_ghostscript", 507 | .type = OBS_SOURCE_TYPE_INPUT, 508 | .output_flags = OBS_SOURCE_VIDEO | OBS_SOURCE_INTERACTION, 509 | .create = pdf_source_create, 510 | .destroy = pdf_source_destroy, 511 | .update = pdf_source_update, 512 | .get_name = pdf_source_get_name, 513 | .get_defaults = pdf_source_defaults, 514 | .get_width = pdf_source_getwidth, 515 | .get_height = pdf_source_getheight, 516 | .video_render = pdf_source_render, 517 | .get_properties = pdf_source_properties, 518 | .key_click = pdf_source_key_click, 519 | .mouse_wheel = pdf_source_mouse_wheel 520 | }; 521 | 522 | OBS_DECLARE_MODULE() 523 | OBS_MODULE_USE_DEFAULT_LOCALE("obs-ghostscript", "en-US") 524 | 525 | bool obs_module_load(void) 526 | { 527 | gsapi_new_instance(&shared_ghostscript_instance, NULL); 528 | gsapi_set_display_callback(shared_ghostscript_instance, &display); 529 | 530 | obs_register_source(&pdf_source_info); 531 | 532 | return true; 533 | } 534 | 535 | void obs_module_unload(void) 536 | { 537 | gsapi_delete_instance(shared_ghostscript_instance); 538 | shared_ghostscript_instance = NULL; 539 | } 540 | --------------------------------------------------------------------------------