├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── FieldtypePDF.module.php ├── FieldtypePDF ├── PDFConverter.php ├── PagePDF.php └── PagePDFs.php ├── InputfieldPDF.css ├── InputfieldPDF.module.php ├── LICENSE ├── README.md ├── apigen.neon ├── composer.json ├── phpunit.xml ├── tense.yml └── test ├── FieldtypePDFTest.php ├── asset ├── test-custom.jpg ├── test.jpg └── test.pdf └── bootstrap.php /.gitignore: -------------------------------------------------------------------------------- 1 | .tense 2 | .ampp 3 | /nbproject 4 | /vendor 5 | /doc 6 | /test/log 7 | /test/config.sh 8 | composer.lock 9 | tense.local.yml 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.6 5 | 6 | env: 7 | global: 8 | secure: LcmdRBX5bX8BbpHewAlUwBCRAzCkUvS+XdvY5Bjlbg2NdbAsQ1nPJfGVVfP2wb/vKOeU++6eZASi6X0MWlm1mdCp/6/nu5jSnFrkP/rokddpsictSL2GyM/C9bp45MZpshy6DqY8zoEfKtu9ZvHtVccdErVRrVlOr3ebcfB2uiw= 9 | 10 | before_script: 11 | - git config --global user.email "travis@travis-ci.org" 12 | - git config --global user.name "Travis" 13 | 14 | script: 15 | - echo "TODO testing" 16 | 17 | after_success: 18 | - wget https://gist.githubusercontent.com/uiii/1fc5373c6f58ba29fb33/raw/generate-api.sh 19 | - export REPO_SLUG=${TRAVIS_REPO_SLUG} 20 | - export API_VERSION=${TRAVIS_BRANCH} 21 | - if [ ${TRAVIS_PULL_REQUEST} = 'false' ]; then sh generate-api.sh; fi 22 | 23 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.0.0 (2020-30-12) 2 | 3 | ### Changed 4 | - Refactor for PW 3.x 5 | 6 | ### Deprecated 7 | - Drop PW 2.x support 8 | 9 | ## 1.2.0 (2020-30-12) 10 | 11 | Use version 1.x for backward compatibility with ProcessWire 2.x 12 | 13 | ### Changes 14 | - Remove PW 3.x related things 15 | 16 | ## 1.1.5 (2018-04-05) 17 | 18 | ### Fixed 19 | - Making PW 3.x namespace fixing reliable. 20 | 21 | ## 1.1.4 (2017-08-08) 22 | 23 | ### Fixed 24 | - Fixed module upgrading on PW 3.x [[issue #12](https://github.com/uiii/ProcessWire-FieldtypePDF/issues/12)] 25 | 26 | ## 1.1.3 (2017-04-19) 27 | 28 | ### Changed 29 | - Use [Tense](https://github.com/uiii/tense) for testing against multiple versions of ProcessWire 30 | 31 | ## 1.1.2 (2016-12-09) 32 | 33 | ### Added 34 | - ProcessWire 3.x support 35 | - Module is installable via Composer 36 | 37 | ### Changed 38 | - Use [PW-Test](https://github.com/uiii/pw-test) for testing against multiple versions of ProcessWire 39 | 40 | ## 1.1.1 (2016-08-26) 41 | 42 | ### Fixed 43 | - Fixed module's installation by classname 44 | 45 | ## 1.1.0 (2016-08-26) 46 | 47 | ### Added 48 | - PDF to image converter is now configurable in admin [[issue #7](https://github.com/uiii/ProcessWire-FieldtypePDF/issues/7)] 49 | - You can specify PDF's page number to generate thumbnail [[issue #3](https://github.com/uiii/ProcessWire-FieldtypePDF/issues/3)] 50 | - Fix bugs [[issue #4](https://github.com/uiii/ProcessWire-FieldtypePDF/issues/4), [issue #6](https://github.com/uiii/ProcessWire-FieldtypePDF/issues/6)] 51 | - Add ApiGen config for API documentation generation 52 | - Add PHPUnit tests 53 | - Add license (MIT) 54 | 55 | ### Deprecated 56 | - Deprecated `thumbnail` method, use `toImage` instead. 57 | - Deprecated `isThumbnail` method, use `isImageOfThis` instead. 58 | - Deprecated `removeThumbnails` method, use `removeImages` instead. 59 | 60 | ## 1.0.1 (2014-05-29) 61 | 62 | ### Added 63 | - Added module requirements check [[issue #2](https://github.com/uiii/ProcessWire-FieldtypePDF/issues/2)] 64 | - Set important ImageMagick settings before conversion [[issue #1](https://github.com/uiii/ProcessWire-FieldtypePDF/issues/1)] 65 | -------------------------------------------------------------------------------- /FieldtypePDF.module.php: -------------------------------------------------------------------------------- 1 | (http://uiii.cz) 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the "Software"), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | 27 | namespace ProcessWire; 28 | 29 | use FieldtypePDF\PagePDF; 30 | use FieldtypePDF\PagePDFs; 31 | use FieldtypePDF\PDFConverter; 32 | 33 | class FieldtypePDF extends FieldtypeFile implements ConfigurableModule 34 | { 35 | protected static $defaults = array( 36 | 'fallbackMode' => false 37 | ); 38 | 39 | public static function getModuleInfo() 40 | { 41 | return array( 42 | 'version' => 201, 43 | 'title' => __('PDF with thumbnail', __FILE__), 44 | 'summary' => __('Field that stores one or more PDF files allowing thumbnail creation.', __FILE__), 45 | 'href' => 'http://modules.processwire.com/modules/fieldtype-pdf', 46 | 'author' => 'Richard Jedlička', 47 | 'installs' => 'InputfieldPDF', 48 | 'autoload' => true, 49 | 'requires' => array( 50 | 'ProcessWire>=3.0.0' 51 | ) 52 | ); 53 | } 54 | 55 | public function init() 56 | { 57 | spl_autoload_register(function($classname) { 58 | $classname = ltrim($classname, '\\'); 59 | $filename = sprintf('%s/%s.php', __DIR__, str_replace('\\', DIRECTORY_SEPARATOR, $classname)); 60 | 61 | if (is_file($filename)) { 62 | require_once $filename; 63 | } 64 | }); 65 | } 66 | 67 | public function ___install() 68 | { 69 | if(! class_exists('Imagick')) { 70 | throw new WireException(__('FieldtypePDF module requires the ImageMagick PHP extension.')); 71 | } 72 | } 73 | 74 | public function set($key, $value) 75 | { 76 | if($key === 'converterImagickOptions' && is_string($value)) { 77 | $value = explode("\n", $value); 78 | } 79 | 80 | return parent::set($key, $value); 81 | } 82 | 83 | public function getBlankValue(Page $page, Field $field) 84 | { 85 | $pagePDFs = new PagePDFs($page); 86 | $pagePDFs->setField($field); 87 | $pagePDFs->setTrackChanges(true); 88 | 89 | return $pagePDFs; 90 | } 91 | 92 | protected function getBlankPagefile(Pagefiles $pagefiles, $filename) 93 | { 94 | return new PagePDF($pagefiles, $filename); 95 | } 96 | 97 | protected function getDefaultFileExtensions() 98 | { 99 | return 'pdf'; 100 | } 101 | 102 | public function ___getConfigInputfields(Field $field) 103 | { 104 | $inputfields = parent::___getConfigInputfields($field); 105 | 106 | // hide input extensions field 107 | $extensionsInputField = $inputfields->get('extensions'); 108 | $extensionsInputField->collapsed = Inputfield::collapsedHidden; 109 | 110 | // add fields for thumbnail creation settings 111 | $converterOptions = PDFConverter::$defaultOptions; 112 | 113 | /** @var InputfieldFieldset */ 114 | $thumbnailFieldset = $this->modules->get('InputfieldFieldset'); 115 | $thumbnailFieldset->label = $this->_('PDF to image converter'); 116 | $thumbnailFieldset->description = $this->_('Options used when creating images from PDF files.'); 117 | 118 | /** @var InputfieldText */ 119 | $formatField = $this->modules->get('InputfieldText'); 120 | $formatField->attr('name', 'converterFormat'); 121 | $formatField->attr('value', $field->converterFormat ?: $converterOptions['format']); 122 | $formatField->label = $this->_('Image format'); 123 | $formatField->description = $this->_('Format used when creating the image (recomeneded are JPEG or PNG). Don\'t forget to check the file extension.'); 124 | $url = 'http://www.imagemagick.org/script/formats.php#supported'; 125 | $formatField->notes = $this->_("For supported formats see [$url]($url)"); 126 | $formatField->required = true; 127 | $thumbnailFieldset->add($formatField); 128 | 129 | /** @var InputfieldText */ 130 | $extensionField = $this->modules->get('InputfieldText'); 131 | $extensionField->attr('name', 'imageExtenstion'); 132 | $extensionField->attr('value', $field->imageExtension ?: PagePDF::$defaultImageExtension); 133 | $extensionField->label = $this->_('File extension'); 134 | $extensionField->description = $this->_('Sould correspond the image format.'); 135 | $extensionField->required = true; 136 | $thumbnailFieldset->add($extensionField); 137 | 138 | if (! $this->fallbackMode) { 139 | /** @var InputfieldText */ 140 | $backgroundField = $this->modules->get('InputfieldText'); 141 | $backgroundField->attr('name', 'converterBackground'); 142 | $backgroundField->attr('value', $field->converterBackground ?: $converterOptions['background']); 143 | $backgroundField->label = $this->_('Image background'); 144 | $backgroundField->description = $this->_('Color used as a background for transparent PDFs. Enter \'transparent\' if you don\'t want the background to be set. Default color is white.'); 145 | $url = 'http://www.imagemagick.org/script/color.php'; 146 | $backgroundField->notes = $this->_("For supported colors see [$url]($url)"); 147 | $thumbnailFieldset->add($backgroundField); 148 | 149 | /** @var InputfieldFieldset */ 150 | $imagickFieldset = $this->modules->get('InputfieldFieldset'); 151 | $imagickFieldset->label = $this->_('ImageMagick settings (advanced)'); 152 | $imagickFieldset->collapsed = Inputfield::collapsedYes; 153 | $imagickFieldset->description = $this->_('Settings set to the ImageMagick instance before reading the PDF file. **Change this only if you know what you are doing.**'); 154 | 155 | /** @var InputfieldText */ 156 | $resolutionField = $this->modules->get('InputfieldText'); 157 | $resolutionField->attr('name', 'converterResolution'); 158 | $resolutionField->attr('value', $field->converterResolution ?: $converterOptions['resolution']); 159 | $resolutionField->label = $this->_('density'); 160 | $url = 'http://www.imagemagick.org/script/command-line-options.php#density'; 161 | $resolutionField->notes = $this->_("see [$url]($url)"); 162 | 163 | /** @var InputfieldSelect */ 164 | $colorspaceField = $this->modules->get('InputfieldSelect'); 165 | $colorspaceField->attr('name', 'converterColorspace'); 166 | $colorspaceField->label = $this->_('Color space'); 167 | $colorspaceField->description = $this->_('Leave empty if you don\'t want to set.'); 168 | 169 | $imagickReflection = new \ReflectionClass('Imagick'); 170 | foreach ($imagickReflection->getConstants() as $name => $value) { 171 | if (preg_match('/^COLORSPACE_([^_]+)$/', $name, $matches)) { 172 | $name = $matches[1]; 173 | 174 | if ($name === 'UNDEFINED') { 175 | $colorspaceField->addOption($value, ''); 176 | } else { 177 | $colorspaceField->addOption($value, $name); 178 | } 179 | } 180 | } 181 | 182 | $colorspaceField->attr('value', $field->converterColorspace === null ? $converterOptions['colorspace'] : $field->converterColorspace); 183 | 184 | if (! PDFConverter::isColorspaceSupported()) { 185 | $colorspaceField->attr('disabled', true); 186 | $colorspaceField->addOption(\Imagick::COLORSPACE_UNDEFINED, 'not supported'); 187 | $colorspaceField->notes = $this->_('Supported since ImageMagick version 6.5.7'); 188 | } 189 | 190 | /** @var InputfieldTextArea */ 191 | $optionsField = $this->modules->get('InputfieldTextarea'); 192 | $optionsField->attr('name', 'converterImagickOptions'); 193 | $optionsField->attr('rows', 3); 194 | $optionsField->label = $this->_('Options'); 195 | $optionsField->description = $this->_('One definition per line (key=value).'); 196 | $url = 'http://www.imagemagick.org/script/command-line-options.php#define'; 197 | $optionsField->notes = $this->_("See [$url]($url)"); 198 | 199 | $optionsField->attr('value', $field->converterImagickOptions ?: implode("\n", $converterOptions['imagickOptions'])); 200 | 201 | $imagickFieldset->append($resolutionField); 202 | $imagickFieldset->append($colorspaceField); 203 | $imagickFieldset->append($optionsField); 204 | 205 | $thumbnailFieldset->add($imagickFieldset); 206 | } else { 207 | $thumbnailFieldset->notes = $this->_( 208 | '**Fallback mode is ON:** This will produce low quality images and some field type options won\'t be available, but may work where normal mode doesn\'t.' . 209 | 'You can turn it OFF [here](' . $this->config->urls->admin . 'module/edit?name=' . $this->name . ').' 210 | ); 211 | } 212 | 213 | $inputfields->add($thumbnailFieldset); 214 | 215 | return $inputfields; 216 | } 217 | 218 | public static function getModuleConfigInputfields(array $data) 219 | { 220 | $data = array_merge(self::$defaults, $data); 221 | 222 | $modules = wire('modules'); 223 | 224 | $inputfields = new InputfieldWrapper; 225 | 226 | $field = $modules->get('InputfieldCheckbox'); 227 | $field->attr('name', 'fallbackMode'); 228 | $field->attr('checked', $data['fallbackMode']); 229 | $field->label = __('Fallback mode'); 230 | $field->description = __('Check this when you have troubles with generating image (e.g. GhostScript errors).'); 231 | $field->notes = __('**Warning:** This will produce low quality images and some field type options won\'t be available, but may work where normal mode doesn\'t.'); 232 | 233 | $inputfields->add($field); 234 | 235 | return $inputfields; 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /FieldtypePDF/PDFConverter.php: -------------------------------------------------------------------------------- 1 | (http://uiii.cz) 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the "Software"), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | 27 | namespace FieldtypePDF; 28 | 29 | use Imagick; 30 | 31 | /** 32 | * PDF to image converter 33 | */ 34 | class PDFConverter 35 | { 36 | /** 37 | * @var array 38 | */ 39 | public static $defaultOptions = array( 40 | 'format' => 'JPEG', 41 | 'extension' => 'jpg', 42 | 'background' => '#FFFFFF', 43 | 'resolution' => '300x300', 44 | 'colorspace' => Imagick::COLORSPACE_RGB, 45 | 'imagickOptions' => array( 46 | 'pdf:use-cropbox=true' 47 | ), 48 | 'fallbackMode' => false 49 | ); 50 | 51 | /** 52 | * @var array 53 | */ 54 | protected $options; 55 | 56 | /** 57 | * 58 | * @var string 59 | */ 60 | protected $pdfFilename; 61 | 62 | /** 63 | * Check if colorspace is supported by ImageMagick. 64 | * 65 | * @return bool 66 | */ 67 | public static function isColorspaceSupported() 68 | { 69 | return method_exists('Imagick', 'setColorspace'); 70 | } 71 | 72 | /** 73 | * Constructor 74 | * 75 | * @param string $pdfFilename PDF file to be converted 76 | * @param array $options Converter options (Optional) 77 | * @see setOptions() 78 | */ 79 | public function __construct($pdfFilename, $options = array()) 80 | { 81 | $this->pdfFilename = $pdfFilename; 82 | $this->setOptions($options); 83 | } 84 | 85 | /** 86 | * Set converter options 87 | * 88 | * @param array $options 89 | * 90 | * Available options are: 91 | * 92 | * - format (string) - format of the image 93 | * - extension (string) - image file extension 94 | * - background (string) - image background (used when the PDF's background is transparent), 95 | * to leave the background transparent set NULL 96 | * - resolution (string) - resolution used when reading the PDF (e.g. '300x300') 97 | * - colorspace (int) - colorspace used when reading the PDF (Imagick::COLORSPACE_* constant) 98 | * - imagickOptions (string[]) - ImageMagick options (each option in format 'key=value') 99 | * - fallbackMode (bool) - Fallback mode (produces low quality images, but may work where normal mode don't) 100 | * 101 | * For converter default options see {@link $defaultOptions} 102 | */ 103 | public function setOptions(array $options) 104 | { 105 | $options = array_replace(self::$defaultOptions, $options); 106 | 107 | if (isset($options['resolution']) && $options['resolution']) { 108 | $resolution = $options['resolution']; 109 | 110 | if (is_string($resolution)) { 111 | $resolution = explode('x', $options['resolution']); 112 | } elseif (is_numeric($resolution)) { 113 | $resolution = array($resolution); 114 | } 115 | 116 | if (is_array($resolution)) { 117 | // append the resolution's second dimension if not set 118 | if (count($resolution) === 1) { 119 | $resolution[] = $resolution[0]; 120 | } 121 | 122 | $options['resolution'] = $resolution; 123 | } 124 | } 125 | 126 | if (isset($options['imagickOptions']) && ! is_array($options['imagickOptions'])) { 127 | $options['imagickOptions'] = array($options['imagickOptions']); 128 | } 129 | 130 | $this->options = $options; 131 | } 132 | 133 | /** 134 | * Get converter options 135 | * 136 | * @return array 137 | */ 138 | public function getOptions() 139 | { 140 | return $this->options; 141 | } 142 | 143 | /** 144 | * Convert PDF to image specified by filename. 145 | * 146 | * @param int $page Number of PDF's page to be converted to image (indexed from 0) 147 | * @param string $imageFilename Filename of output image 148 | */ 149 | public function toImage($page, $imageFilename) 150 | { 151 | $options = $this->options; 152 | 153 | $imagick = new Imagick(); 154 | $backgroundImagick = new Imagick(); 155 | 156 | if ($options['fallbackMode']) { 157 | $imagick->clear(); 158 | $imagick = new Imagick(sprintf('%s[%s]', $this->pdfFilename, $page)); 159 | } else { 160 | if ($options['resolution']) { 161 | $resolution = $options['resolution']; 162 | $imagick->setResolution($resolution[0], $resolution[1]); 163 | $backgroundImagick->setResolution($resolution[0], $resolution[1]); 164 | } 165 | 166 | foreach($options['imagickOptions'] as $defition) { 167 | $defition = explode('=', $defition); 168 | $imagick->setOption($defition[0], $defition[1]); 169 | } 170 | 171 | if ($options['colorspace'] && self::isColorspaceSupported()) { 172 | $imagick->setColorspace($options['colorspace']); 173 | } 174 | 175 | $imagick->readimage(sprintf('%s[%s]', $this->pdfFilename, $page)); 176 | } 177 | 178 | $image = $imagick; 179 | 180 | if ($options['background'] !== 'transparent') { 181 | $backgroundImagick->newImage( 182 | $imagick->getimagewidth(), 183 | $imagick->getimageheight(), 184 | $options['fallbackMode'] ? self::$defaultOptions['background'] : $options['background'] 185 | ); 186 | 187 | $backgroundImagick->compositeimage($imagick, Imagick::COMPOSITE_OVER, 0, 0); 188 | $image = $backgroundImagick; 189 | } 190 | 191 | $image->writeImage(sprintf('%s:%s', $options['format'], $imageFilename)); 192 | 193 | $backgroundImagick->clear(); 194 | $imagick->clear(); 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /FieldtypePDF/PagePDF.php: -------------------------------------------------------------------------------- 1 | (http://uiii.cz) 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the "Software"), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | 27 | namespace FieldtypePDF; 28 | 29 | use DirectoryIterator; 30 | use Exception; 31 | 32 | use ProcessWire\Pagefile; 33 | use ProcessWire\Pageimage; 34 | use ProcessWire\Pageimages; 35 | 36 | /** 37 | * Represents a single PDF file item attached to a page, typically via a FieldtypePDF field. 38 | */ 39 | class PagePDF extends Pagefile 40 | { 41 | public static $defaultImageExtension = 'jpg'; 42 | 43 | protected $options = array(); 44 | 45 | protected $images; 46 | 47 | /** 48 | * Construct a new PagePDF 49 | * 50 | * @param PagePDFs $pagefiles Owning collection 51 | * @param string $filename Full path and filename to this pagefile 52 | */ 53 | public function __construct(PagePDFs $pagefiles, $filename) 54 | { 55 | parent::__construct($pagefiles, $filename); 56 | 57 | $field = $pagefiles->getField(); 58 | foreach($field->getArray() as $key => $value) { 59 | if (preg_match('/^converter(.+)$/', $key, $matches)) { 60 | $this->options[lcfirst($matches[1])] = $value; 61 | } 62 | } 63 | 64 | if ($field->get('imageExtension')) { 65 | $this->options['extension'] = $field->get('imageExtension'); 66 | } 67 | 68 | if ($field->type->fallbackMode) { 69 | $this->options['fallbackMode'] = true; 70 | } 71 | 72 | $this->images = new Pageimages($this->pagefiles->getPage()); 73 | } 74 | 75 | /** 76 | * Convert PDF to image 77 | * 78 | * This generates one image for each combinations of $page and $options['suffix']. 79 | * If the file already exists it isn't regenerated until $options['forceNew'] is TRUE. 80 | * 81 | * @param int $page Number of PDF's page (indexed from 0) to be converted to image 82 | * @param type $options 83 | * 84 | * Available options are: 85 | * 86 | * - sufix (string[]) - Suffixes to be used in image's filename 87 | * - forceNew (boolean) - Whether to overwrite the image if already exists 88 | * 89 | * Accepts also converter options, see {@link FieldtypePDF\PDFConverter::setOptions}. 90 | * 91 | * @return Pageimage 92 | */ 93 | public function ___toImage($page = 0, $options = array()) 94 | { 95 | if (is_array($page)) { 96 | $options = $page; 97 | $page = 0; 98 | } 99 | 100 | $defaultOptions = array( 101 | 'extension' => self::$defaultImageExtension, 102 | 'suffix' => array(), 103 | 'forceNew' => false, 104 | ); 105 | 106 | if ($page > 0) { 107 | $defaultOptions['suffix'][] = 'page' . $page; 108 | } 109 | 110 | $options = array_replace($defaultOptions, $this->options, $options); 111 | 112 | $suffixStr = ''; 113 | if(!empty($options['suffix'])) { 114 | $suffix = is_array($options['suffix']) ? $options['suffix'] : array($options['suffix']); 115 | sort($suffix); 116 | foreach($suffix as $key => $s) { 117 | $s = strtolower($this->wire('sanitizer')->fieldName($s)); 118 | if(empty($s)) unset($suffix[$key]); 119 | else $suffix[$key] = $s; 120 | } 121 | if(count($suffix)) $suffixStr = '-' . implode('-', $suffix); 122 | } 123 | 124 | // e.g. myfile.pdf -> myfile-page2.jpg 125 | $basename = sprintf('%s%s.%s', 126 | basename($this->basename(), "." . $this->ext()), 127 | $suffixStr, 128 | $options['extension'] 129 | ); 130 | 131 | $filename = $this->pagefiles->path() . $basename; 132 | $exists = file_exists($filename); 133 | 134 | if(! $exists || $options['forceNew']) { 135 | if($exists && $options['forceNew']) { 136 | $image = new Pageimage($this->images, $filename); 137 | $image->unlink(); 138 | } 139 | 140 | try { 141 | $converter = new PDFConverter($this->filename, $options); 142 | $converter->toImage($page, $filename); 143 | 144 | if($this->config->chmodFile) { 145 | chmod($filename, octdec($this->config->chmodFile)); 146 | } 147 | } catch(Exception $e) { 148 | if ($this->pagefiles->getPage()->template === 'admin') { 149 | $this->error($e->getMessage()); 150 | $this->error("PDF to image conversion failed for $filename"); 151 | } else { 152 | throw $e; 153 | } 154 | } 155 | } 156 | 157 | $image = new Pageimage($this->images, $filename); 158 | $this->images->add($image); 159 | return $image; 160 | } 161 | 162 | /** 163 | * Test whether $basename is image generated from this PDF 164 | * 165 | * @param type $basename 166 | * @return boolean 167 | */ 168 | public function isImageOfThis($basename) 169 | { 170 | $imageName = basename($basename); 171 | $originalName = basename($this->basename, "." . $this->ext()); // excludes extension 172 | 173 | $re = '/^' 174 | . $originalName // myfile 175 | . '(?:-([-_a-zA-Z0-9]+))?' // -suffix1 or -suffix1-suffix2, etc. 176 | . '\.[^.]+' // .jpg 177 | . '$/'; 178 | 179 | // if regex does not match or file is PDF, return false 180 | if(! preg_match($re, $imageName) || preg_match('/^.*pdf$/', $imageName)) { 181 | return false; 182 | } 183 | 184 | return true; 185 | } 186 | 187 | /** 188 | * Get all images generated from this PDF 189 | * 190 | * @return Pageimages 191 | */ 192 | public function getImages() 193 | { 194 | $images = new Pageimages($this->pagefiles->page); 195 | $dir = new DirectoryIterator($this->pagefiles->path); 196 | 197 | foreach($dir as $file) { 198 | if($file->isDir() || $file->isDot()) continue; 199 | if(! $this->isImageOfThis($file->getFilename())) continue; 200 | $images->add($file->getFilename()); 201 | } 202 | 203 | return $images; 204 | } 205 | 206 | /** 207 | * Remove all generated images. 208 | * 209 | * @return void 210 | */ 211 | public function removeImages() 212 | { 213 | $images = $this->getImages(); 214 | 215 | foreach($images as $image) { 216 | $image->unlink(); 217 | } 218 | 219 | return $this; 220 | } 221 | 222 | /** 223 | * Delete the physical file on disk associated with this PDF. 224 | * 225 | * Unlinks also all generated images. 226 | * 227 | * @return bool 228 | */ 229 | public function unlink() 230 | { 231 | $this->removeImages(); 232 | return parent::unlink(); 233 | } 234 | 235 | /** 236 | * @deprecated since version 1.1.0, please use toImage() instead 237 | * @param int $width 238 | * @param int $height 239 | * @return Pageimage 240 | */ 241 | public function thumbnail($width, $height = 0) 242 | { 243 | $height = $height ?: $width; 244 | 245 | $image = $this->toImage(); 246 | return $image->size($width, $height); 247 | } 248 | 249 | /** 250 | * @deprecated since version 1.1.0, please use isImageOfThis() instead 251 | * @param string $basename 252 | */ 253 | public function isThumbnail($basename) 254 | { 255 | $images = $this->getImages(); 256 | 257 | if ($images->count() == 0) { 258 | $images->add($this->toImage()); 259 | } 260 | 261 | foreach($images as $image) { 262 | if($image->basename === $basename || $image->isVariation($basename)) { 263 | return true; 264 | } 265 | } 266 | 267 | return false; 268 | } 269 | 270 | /** 271 | * @deprecated since version 1.1.0, please use removeImages() instead 272 | */ 273 | public function removeThumbnails() 274 | { 275 | $this->removeImages(); 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /FieldtypePDF/PagePDFs.php: -------------------------------------------------------------------------------- 1 | (http://uiii.cz) 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the "Software"), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | 27 | namespace FieldtypePDF; 28 | 29 | use ProcessWire\Pagefiles; 30 | 31 | /** 32 | * PagePDFs are a collection of PagePDF objects. 33 | * 34 | * Typically a PagePDFs object will be associated with a specific field attached to a Page. 35 | * There may be multiple instances of PagePDFs attached to a given Page (depending on what fields are in it's fieldgroup). 36 | */ 37 | class PagePDFs extends Pagefiles 38 | { 39 | public function isValidItem($item) 40 | { 41 | return $item instanceof PagePDF; 42 | } 43 | 44 | public function makeBlankItem() 45 | { 46 | return new PagePDF($this, ''); 47 | } 48 | 49 | public function add($item) 50 | { 51 | if(is_string($item)) { 52 | $item = new PagePDF($this, $item); 53 | } 54 | 55 | return parent::add($item); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /InputfieldPDF.css: -------------------------------------------------------------------------------- 1 | .InputfieldPDF img { 2 | max-width: 90%; 3 | } 4 | 5 | .InputfieldPDF a.InputfieldFileLink + label.InputfieldFileDescription, 6 | .InputfieldPDF a.InputfieldFileLink + label.InputfieldFileTags { 7 | margin-top: 0.5em; 8 | } 9 | 10 | .InputfieldPDF a.InputfieldFileMove span.ui-icon { 11 | float: right; 12 | position: relative; 13 | top: 2px; 14 | } 15 | .InputfieldPDF a.InputfieldFileMove { 16 | float: right; 17 | width: 20px; 18 | padding-right: 20px; 19 | visibility: hidden; 20 | } 21 | 22 | .ui-state-hover a.InputfieldFileMove { 23 | visibility: visible; 24 | } 25 | .ui-state-error a.InputfieldFileMove { 26 | display: none; 27 | } 28 | 29 | 30 | /** 31 | * Grid mode 32 | * 33 | */ 34 | .InputfieldPDF .InputfieldHeader i.icon-th, 35 | .InputfieldPDF .InputfieldHeader i.icon-list { 36 | font-size: 14px; 37 | } 38 | 39 | .InputfieldImageGrid .InputfieldContent { 40 | padding-right: 0; 41 | } 42 | 43 | .InputfieldImageGrid .InputfieldFileInfo { 44 | display: none; 45 | } 46 | 47 | .InputfieldImageGrid .AjaxUpload .InputfieldFileInfo { 48 | display: block; 49 | clear: both; 50 | float: none; 51 | margin-top: 1em; 52 | } 53 | 54 | .InputfieldImageGrid .InputfieldFileData label { 55 | display: none; 56 | } 57 | 58 | .InputfieldImageGrid .InputfieldFileList .langTabsContainer, 59 | .InputfieldImageGrid .InputfieldFileList .InputfieldFileDescription, 60 | .InputfieldImageGrid .InputfieldFileList .InputfieldFileTags { 61 | display: none !important; 62 | } 63 | 64 | .InputfieldImageGrid .InputfieldFileList .InputfieldImage, 65 | .InputfieldImageGrid .InputfieldFileList .InputfieldFileData { 66 | padding: 0; 67 | margin: 0; 68 | float: left; 69 | background: none; 70 | border: none; 71 | } 72 | 73 | .InputfieldImageGrid .InputfieldFileList .InputfieldFileData a.InputfieldFileLink { 74 | overflow: hidden; 75 | width: 100px; 76 | height: 100px; 77 | background: #000; 78 | margin: 0 5px 5px 0; 79 | display: block; 80 | float: left; 81 | cursor: move; 82 | } 83 | .InputfieldImageGrid .InputfieldFileList .InputfieldFileData a.InputfieldFileLink img { 84 | max-width: 9999px; 85 | width: auto; 86 | height: 100px; 87 | display: block; 88 | margin: 0 auto; 89 | display: none; 90 | } 91 | 92 | .Inputfields .InputfieldImageGrid > .InputfieldContent > .InputfieldFileList > li { 93 | margin: 0; 94 | } 95 | 96 | .InputfieldImageGrid .InputfieldFileUpload { 97 | padding-top: 1em; 98 | clear: both; 99 | 100 | } 101 | 102 | .InputfieldPDF .InputfieldHeader .InputfieldImageListToggle { 103 | float: right; 104 | padding-right: 1em; 105 | position: relative; 106 | } 107 | .InputfieldStateCollapsed .InputfieldHeader .InputfieldImageListToggle { 108 | display: none; 109 | } 110 | 111 | -------------------------------------------------------------------------------- /InputfieldPDF.module.php: -------------------------------------------------------------------------------- 1 | (http://uiii.cz) 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the "Software"), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | 27 | namespace ProcessWire; 28 | 29 | use FieldtypePDF\PagePDF; 30 | 31 | class InputfieldPDF extends InputfieldFile implements InputfieldItemList 32 | { 33 | public static function getModuleInfo() 34 | { 35 | return array( 36 | 'version' => 201, 37 | 'title' => __('PDF files with thumbnails', __FILE__), // Module Title 38 | 'summary' => __('One or more PDF files upload with thumbnails', __FILE__), // Module Summary 39 | 'href' => 'http://modules.processwire.com/modules/fieldtype-pdf', 40 | 'author' => "Richard Jedlička", 41 | 'requires' => array( 42 | 'ProcessWire>=3.0.0', 43 | 'FieldtypePDF' 44 | ) 45 | ); 46 | } 47 | 48 | public function init() 49 | { 50 | parent::init(); 51 | $this->set('extensions', 'PDF'); 52 | $this->set('adminThumbs', false); 53 | 54 | $options = $this->wire('config')->adminThumbOptions; 55 | if(!is_array($options)) $options = array(); 56 | if(empty($options['width']) && empty($options['height'])) $options['height'] = 100; // default 57 | $this->set('adminThumbWidth', empty($options['width']) ? 0 : (int) $options['width']); 58 | $this->set('adminThumbHeight', empty($options['height']) ? 0 : (int) $options['height']); 59 | $this->set('adminThumbScale', empty($options['scale']) ? 1.0 : (float) $options['scale']); 60 | } 61 | 62 | public function getAdminThumb(PagePDF $pdf) 63 | { 64 | $thumb = $pdf->toImage(); 65 | $thumbInfo = array(); 66 | 67 | /** @var InputfieldImage */ 68 | $inputfieldImage = $this->modules->get('InputfieldImage'); 69 | if (method_exists($inputfieldImage, 'getAdminThumb')) { // PW 2.6+ 70 | $inputfieldImage->set('adminThumbs', true); 71 | return $inputfieldImage->getAdminThumb($pdf->toImage()); 72 | } else { 73 | $error = ''; 74 | $thumbAttrs = array(); 75 | 76 | if($this->adminThumbs) { 77 | $thumbHeight = $thumb->height; 78 | if($thumbHeight > $this->adminThumbHeight) { 79 | // create a variation for display with this inputfield 80 | $thumb = $thumb->height($this->adminThumbHeight); 81 | if($thumb->error) $error = "$thumb->error"; 82 | $thumbHeight = $this->adminThumbHeight; 83 | } 84 | $thumbAttrs['height'] = $thumbHeight; 85 | $thumbAttrs['width'] = $thumb->width; 86 | } 87 | 88 | $thumbAttrs['src'] = $thumb->url; 89 | 90 | // ensure cached image doesn't get shown when replacing same filename 91 | if($this->overwrite) $thumbAttrs['src'] .= "?m=" . filemtime($pdf->pathname); 92 | 93 | $thumbInfo['attr'] = $thumbAttrs; 94 | $thumbInfo['error'] = $error; 95 | 96 | $markup = " $value) $markup .= "$key=\"$value\" "; 98 | $markup .= " />"; 99 | 100 | $thumbInfo['markup'] = $markup; 101 | } 102 | 103 | $thumbInfo['thumb'] = $thumb; 104 | 105 | return $thumbInfo; 106 | } 107 | 108 | 109 | public function ___render() { 110 | $this->wire('modules')->loadModuleFileAssets('InputfieldFile'); 111 | 112 | return parent::___render(); 113 | } 114 | 115 | protected function ___renderItem($pagefile, $id, $n) 116 | { 117 | $thumb = $this->getAdminThumb($pagefile); 118 | $error = $thumb['error'] ? "" . $this->wire('sanitizer')->entities($thumb['error']) . "" : ""; 119 | 120 | $out = "\n\t\t

"; 121 | 122 | if (function_exists('wireIconMarkupFile')) { // PW 2.6+ 123 | $out .= "\n\t\t\t" . wireIconMarkupFile($pagefile->basename, "fa-fw HideIfEmpty"); 124 | } else { 125 | $out .= "\n\t\t\t" . 126 | "\n\t\t\t"; 127 | } 128 | 129 | $out .= "\n\t\t\t{$pagefile->basename} " . 130 | "\n\t\t\t" . str_replace(' ', ' ', $pagefile->filesizeStr) . " " . 131 | "\n\t\t\t"; 134 | 135 | $out .= "\n\t\t

"; // .InputfieldFileInfo.InputfieldItemHeader 136 | 137 | $out .= "\n\t\t
"; 138 | 139 | if ($this->adminThumbs) { 140 | $out .= "\n\t\t\t
" . 141 | "\n\t\t\t\t$thumb[markup]" . 142 | "\n\t\t\t
"; 143 | } 144 | 145 | $out .= "\n\t\t\t" . $error . $this->renderItemDescriptionField($pagefile, $id, $n) . 146 | "\n\t\t\t"; 147 | 148 | $out .= "\n\t\t
"; // . InputfieldFileData 149 | 150 | return $out; 151 | } 152 | 153 | public function ___getConfigInputfields() 154 | { 155 | $inputfields = parent::___getConfigInputfields(); 156 | 157 | /** @var InputfieldCheckbox */ 158 | $field = $this->modules->get('InputfieldCheckbox'); 159 | $field->attr('name', 'adminThumbs'); 160 | $field->attr('value', 1); 161 | $field->attr('checked', $this->adminThumbs ? 'checked' : ''); 162 | $field->label = $this->_('Display thumbnails in page editor?'); 163 | $inputfields->add($field); 164 | 165 | return $inputfields; 166 | } 167 | 168 | /** 169 | * Specify which config inputfields can be used with template/field contexts 170 | * 171 | * PW 2.6+ 172 | * 173 | * @param Field $field 174 | * @return array 175 | * 176 | */ 177 | public function ___getConfigAllowContext($field) 178 | { 179 | $fieldNames = method_exists(get_parent_class(), '___getConfigAllowContext') 180 | ? parent::___getConfigAllowContext($field) 181 | : array(); 182 | 183 | return array_merge($fieldNames, array( 184 | 'adminThumbs' 185 | )); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright 2016 Richard Jedlička (http://uiii.cz) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PDF Fieldtype/Inputfield 2 | 3 | [![Packagist](https://img.shields.io/packagist/v/uiii/processwire-fieldtypepdf.svg)](https://packagist.org/packages/uiii/processwire-fieldtypepdf) 4 | 5 | Module for [ProcessWire CMS](https://processwire.com) allowing you to easily generate images from the PDF files embedded to the site. 6 | 7 | 1. [Requirements](#requirements) 8 | 2. [Installation](#installation) 9 | 3. [How to use](#how-to-use) 10 | - [In site's administration](#in-sites-administration) 11 | - [In templates](#in-templates) 12 | 4. [API documentation](#api-documentation) 13 | 5. [Tests](#tests) 14 | 6. [Upgrading from 1.0.1 and lower](#upgrading-from-101-and-lower) 15 | 7. [Troubleshooting](#troubleshooting) 16 | 8. [Changelog](CHANGELOG.md) 17 | 18 | ## Requirements 19 | 20 | - Processwire 3+ 21 | - ImageMagick PHP extension 22 | - Ghostscript 23 | 24 | > For ProcessWire 2.x support use version 1.x, maintained in [pw-2](https://github.com/uiii/ProcessWire-FieldtypePDF/tree/pw-2) branch. 25 | 26 | ## Installation 27 | 28 | [How to install or uninstall modules](http://modules.processwire.com/install-uninstall/). 29 | 30 | ### Via Composer 31 | In your ProcessWire installation root run: 32 | ``` 33 | composer require uiii/processwire-fieldtypepdf 34 | ``` 35 | Login to your ProcessWire admin and go to *Modules* > *Refresh* and install the module. 36 | 37 | If you want to read more about ProcessWire and Composer visit [https://processwire.com/blog/posts/composer-google-calendars-and-processwire/](https://processwire.com/blog/posts/composer-google-calendars-and-processwire/) 38 | 39 | ## How to use 40 | 41 | ### In site's administration 42 | 43 | Add a field and set its type to `PDF`. 44 | Use the field the same way as the file field (obviously, this accepts only \*.pdf files). 45 | After the file is uploaded you will see a small thumbnail of it, just like for image field. 46 | 47 | Image generation is highly configurable (image format, extension, background, ...). See the *PDF to image converter* section on field's *Details* tab. 48 | 49 | ![Field settings](http://i.imgbox.com/xP1dt37q) 50 | 51 | ### In templates 52 | 53 | > There are some backward-compatible API changes against the version 1.0.1 and lower, see [Upgrading from 1.0.1 and lower](#upgrading-from-101-and-lower). 54 | 55 | The PDF field extends file field and adds new hookable [`___toImage($page = 0, $options = array())`](http://uiii.github.io/ProcessWire-FieldtypePDF/dev/class-FieldtypePDF.PagePDF.html#____toImage) method to generate the image from PDF. 56 | 57 | ```php 58 | $image = $page->pdfFile->toImage(); 59 | $image->size(100, 100); 60 | ``` 61 | Method accepts two optional parameters. First is the `$page`, which specifies the PDF's page number the image is generated from, default is 0. The exception is thrown if the page is out of range. 62 | 63 | The second is `$options` parameter, which is an array of options to override the options set in administration. 64 | ```php 65 | $options = array( 66 | 'suffix' => array('suffix1', 'suffix2'), // suffixes used in filename 67 | 'forceNew' => false, // if TRUE the image is regenerated if already exists 68 | 'format' => 'JPEG', // image format 69 | 'extension' => 'jpg', // image file extension 70 | 'background' => '#FFFFFF', // background color used when the PDF has transparent background 71 | 'resolution' => '300x300', // resolution used when reading the PDF 72 | 'colorspace' => Imagick::COLORSPACE_RGB, // colorspace used when reading the PDF 73 | 'imagickOptions' => array( // ImageMagick options 74 | 'pdf:use-cropbox=true' 75 | ) 76 | ) 77 | ``` 78 | 79 | For each combinations of *page* and *suffixes* there will be one image. The generated images are saved in page's assets and will be **created only once** until *forceNew* options is TRUE. The image is the instance of `Pageimage`, so you can do with it whatever you can do with the image field. When you delete the PDF file the generated images are deleted too. 80 | 81 | ## API documentation 82 | 83 | Generate into *doc* directory: 84 | ``` 85 | apigen generate -d doc 86 | ``` 87 | 88 | ## Tests 89 | 90 | > **DO NOT** run the tests against the production site. They modify the fields, templates and pages as they need, so can potentially damage your site! 91 | 92 | Prepare the PW testing installation and export the `PW_PATH` environment variable containing the path to the root of that installation. Copy the module sources in the `$PW_PATH/site/modules/FieldtypePDF` directory. 93 | 94 | Install required packages: 95 | ``` 96 | composer install 97 | ``` 98 | 99 | Run the tests 100 | ``` 101 | ./vendor/bin/phpunit 102 | ``` 103 | 104 | ### Test multiple ProcessWire versions (automatically) 105 | 106 | You can also automatically test against multiple ProcessWire versions. 107 | It uses [Tense](https://github.com/uiii/tense) tool for it. 108 | 109 | 1. Install reuquired packages: 110 | 111 | ``` 112 | composer install 113 | ``` 114 | 115 | 2. Run the tests: 116 | 117 | ``` 118 | vendor/bin/tense run 119 | ``` 120 | 121 | > **WARNING**: The tool will ask you for database connection parameters. 122 | > Configure the `db` connection parameters carefully because it 123 | > creates and drops a database for each ProcessWire installation. 124 | 125 | ## Upgrading from 1.0.1 and lower 126 | 127 | In 1.1.0 some methods of class PagePDF are deprecated. See the list [here](http://uiii.github.io/ProcessWire-FieldtypePDF/dev/deprecated.html). You doesn't have to make any changes but it is recommended to use the new API, for compatibility with later versions. 128 | 129 | Instructions for replacing the deprecated methods: 130 | 131 | - `$page->pdf->thumbnail($width, $height)` replace with the code 132 | 133 | ```php 134 | $image = $page->pdf->toImage(); 135 | $image->size($width, $height); 136 | ``` 137 | 138 | - `isThumbnail($basename)` replace with `isImageOfThis($basename)` 139 | 140 | > NOTE: There is certain incompatibility between these two methods. While `isThumbnail` returns TRUE for all the images generated from the PDF and also theirs derivatives (e.g. *pdf.jpg*, *pdf.100x100.jpg*), the `isImageOfThis` return TRUE only for the images generated directly from PDF (e.g. *pdf.jpg*). That doesn't change much, because you can use it in combination with `Pageimage::isVariation`. 141 | 142 | - `removeThumbnails` replace with `removeImages` 143 | 144 | ## Troubleshooting 145 | 146 | ### Thumbnail's colors do not match the colors in PDF 147 | 148 | To fix that, you need to made some changes in ImageMagick delegate files. Detailed instructions can be found here: http://www.lassosoft.com/CMYK-Colour-Matching-with-ImageMagick 149 | 150 | ### GhostScript exceptions occured 151 | 152 | If you got some *GhostScript* exceptions when generating image, update *GhostScript* and *ImageMagick* to the latest versions. 153 | 154 | If you can't, you can use the **fallback mode**. Turn it on in the module's settings. 155 | > Be aware of that this will produce low quality images and most of the field type options won't be abvailable. 156 | -------------------------------------------------------------------------------- /apigen.neon: -------------------------------------------------------------------------------- 1 | source: . 2 | title: FieldtypePDF 3 | deprecated: true 4 | extensions: [php, module] 5 | exclude: [doc, test, vendor] 6 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "uiii/processwire-fieldtypepdf", 3 | "description": "Fieldtype/Inputfield module for ProcessWire allowing easy generation of thumbnails of the PDF files", 4 | "keywords": ["processwire", "module"], 5 | "type": "pw-module", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Richard Jedlička", 10 | "email": "jedlicka.r@gmail.com" 11 | } 12 | ], 13 | "require": { 14 | "hari/pw-module": "^1.0" 15 | }, 16 | "require-dev": { 17 | "uiii/tense": "^1.0", 18 | "phpunit/phpunit": "^9.6", 19 | "processwire/processwire": "3.0.34" 20 | }, 21 | "config": { 22 | "allow-plugins": { 23 | "hari/pw-module": true 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | ./test 11 | 12 | 13 | 14 | 23 | 24 | 25 | 26 | . 27 | . 28 | 29 | ./test 30 | ./vendor 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /tense.yml: -------------------------------------------------------------------------------- 1 | testTags: 2 | - '2.5' 3 | - '2.6' 4 | - '2.7' 5 | - '3.0' 6 | testCmd: vendor/bin/phpunit 7 | copySources: 8 | - { destination: site/modules/FieldtypePDF, source: [FieldtypePDF, InputfieldPDF.css, InputfieldPDF.module] } 9 | - { destination: site/modules/FieldtypePDF/FieldtypePDF.module, source: FieldtypePDF.module } 10 | -------------------------------------------------------------------------------- /test/FieldtypePDFTest.php: -------------------------------------------------------------------------------- 1 | isInstalled(self::FIELDTYPE_MODULE_NAME)) { 51 | error(sprintf('Module \'%s\' must not be installed', self::FIELDTYPE_MODULE_NAME)); 52 | } 53 | 54 | if(! $modules->isInstallable(self::FIELDTYPE_MODULE_NAME, true)) { 55 | error(sprintf('Module \'%s\' must be present in the modules directory and all of it\'s dependencies must be installed', self::FIELDTYPE_MODULE_NAME)); 56 | } 57 | } 58 | 59 | public static function tearDownAfterClass() 60 | { 61 | self::clean(); 62 | } 63 | 64 | public function testInstall() 65 | { 66 | $modules = wire('modules'); 67 | 68 | $this->assertTrue($modules->isInstallable(self::FIELDTYPE_MODULE_NAME)); 69 | 70 | $modules->install(self::FIELDTYPE_MODULE_NAME); 71 | $modules->triggerInit(); 72 | 73 | $this->assertTrue($modules->isInstalled(self::FIELDTYPE_MODULE_NAME)); 74 | $this->assertTrue($modules->isInstalled(self::INPUTFIELD_MODULE_NAME)); 75 | 76 | FieldtypePDF\PDFConverter::$defaultOptions['resolution'] = '50x50'; // for speed 77 | } 78 | 79 | /** 80 | * @depends testInstall 81 | */ 82 | public function testAddField() 83 | { 84 | $field = new Field(); 85 | $field->name = self::$fieldname; 86 | $field->type = wire('modules')->get(self::FIELDTYPE_MODULE_NAME); 87 | $field->save(); 88 | 89 | $template = wire('templates')->get('home'); 90 | $template->fieldgroup->add($field); 91 | $template->save(); 92 | 93 | $this->assertTrue(wire('fields')->has($field->name)); 94 | } 95 | 96 | /** 97 | * @depends testAddField 98 | */ 99 | public function testAddFile() 100 | { 101 | $home = wire('pages')->get('/'); 102 | 103 | $pdfFiles = $home->{self::$fieldname}; 104 | $pdfFiles->add(TEST_ASSET_PATH . 'test.pdf'); 105 | $home->save(); 106 | 107 | $this->assertFileExists($pdfFiles->first()->filename); 108 | $this->assertStringStartsWith($pdfFiles->path, $pdfFiles->first()->filename); 109 | 110 | return $pdfFiles; 111 | } 112 | 113 | /** 114 | * @depends testAddFile 115 | */ 116 | public function testCreateImage($pdfFiles) 117 | { 118 | $image = $pdfFiles->first()->toImage(); 119 | 120 | $this->assertInstanceOf(Pageimage, $image); 121 | 122 | $generatedImage = new Imagick($image->filename); 123 | $testImage = new Imagick(TEST_ASSET_PATH . 'test.jpg'); 124 | 125 | // images have the same size 126 | $this->assertEquals($generatedImage->getimagewidth(), $testImage->getimagewidth()); 127 | $this->assertEquals($generatedImage->getimageheight(), $testImage->getimageheight()); 128 | 129 | // images differ lesser than 0.5% 130 | $this->assertLessThan(0.005, $result = $generatedImage->compareimages($testImage, Imagick::METRIC_MEANABSOLUTEERROR)[1]); 131 | 132 | return $pdfFiles; 133 | } 134 | 135 | /** 136 | * @depends testAddFile 137 | */ 138 | public function testCreateImageWithCustomOptions($pdfFiles) 139 | { 140 | $options = array( 141 | 'suffix' => 'custom', 142 | 'extenstion' => 'jpg', 143 | 'format' => 'PNG', 144 | 'background' => '#00FF00', 145 | 'resolution' => '100x100', 146 | ); 147 | 148 | $image = $pdfFiles->first()->toImage($options); 149 | 150 | $generatedImage = new Imagick($image->filename); 151 | $testImage = new Imagick(TEST_ASSET_PATH . 'test-custom.jpg'); 152 | 153 | $this->assertEquals($options['format'], $generatedImage->getimageformat()); 154 | $this->assertEquals($options['resolution'], implode('x', $generatedImage->getimageresolution())); 155 | 156 | // images have the same size 157 | $this->assertEquals($generatedImage->getimagewidth(), $testImage->getimagewidth()); 158 | $this->assertEquals($generatedImage->getimageheight(), $testImage->getimageheight()); 159 | 160 | // images differ lesser than 0.5% 161 | $this->assertLessThan(0.005, $result = $generatedImage->compareimages($testImage, Imagick::METRIC_MEANABSOLUTEERROR)[1]); 162 | 163 | return $pdfFiles; 164 | } 165 | 166 | /** 167 | * @depends testAddFile 168 | */ 169 | public function testDeprecatedMethods($pdfFiles) 170 | { 171 | $pdfFile = $pdfFiles->first(); 172 | 173 | $image = $pdfFile->toImage()->size(10, 10); 174 | $depracatedImage = $pdfFile->thumbnail(10, 10); 175 | 176 | $this->assertEquals($image->filename(), $depracatedImage->filename()); 177 | $this->assertTrue($pdfFile->isThumbnail($depracatedImage)); 178 | 179 | $depracatedImageBasename = $depracatedImage->basename; 180 | $pdfFile->removeImages(); 181 | $this->assertTrue($pdfFile->isThumbnail($depracatedImageBasename)); 182 | } 183 | 184 | /** 185 | * @depends testCreateImageWithCustomOptions 186 | */ 187 | public function testDeleteFile($pdfFiles) 188 | { 189 | $filenames = array(); 190 | 191 | $pdfFile = $pdfFiles->first(); 192 | $filenames[] = $pdfFile->filename; 193 | 194 | // test file's image removal 195 | $image = $pdfFile->toImage(); 196 | $filenames[] = $image->filename; 197 | 198 | // create thumbnail to also test it's removal 199 | $thumbnail = $image->size(10, 10); 200 | $filenames[] = $thumbnail->filename; 201 | 202 | $pdfFiles->remove($pdfFile); 203 | $pdfFiles->page->save(); 204 | 205 | foreach ($filenames as $filename) { 206 | $this->assertFileNotExists($filename); 207 | } 208 | } 209 | 210 | /** 211 | * @depends testInstall 212 | */ 213 | public function testPDFConverter() 214 | { 215 | $converter = new FieldtypePDF\PDFConverter(''); 216 | $converter->setOptions(array( 217 | 'resolution' => 200, 218 | 'imagickOptions' => 'option' 219 | )); 220 | 221 | $options = $converter->getOptions(); 222 | 223 | $this->assertEquals(array(200, 200), $options['resolution']); 224 | $this->assertEquals(array('option'), $options['imagickOptions']); 225 | } 226 | 227 | /** 228 | * @depends testInstall 229 | */ 230 | public function testPagePDFs() 231 | { 232 | $home = wire('pages')->get('/'); 233 | $pdfFiles = $home->{self::$fieldname}; 234 | $blankItem = $pdfFiles->makeBlankItem(); 235 | 236 | $this->assertInstanceOf('FieldtypePDF\PagePDF', $blankItem); 237 | } 238 | 239 | /** 240 | * @depends testInstall 241 | */ 242 | public function testConfigInputfields() 243 | { 244 | $field = new Field(); 245 | $field->type = wire('modules')->get(self::FIELDTYPE_MODULE_NAME); 246 | 247 | $this->assertNotEmpty($field->getConfigInputfields()); 248 | } 249 | 250 | protected static function clean() 251 | { 252 | $modules = wire('modules'); 253 | 254 | $fields = wire('fields'); 255 | foreach ($fields->find('name^=' . self::FIELDTYPE_MODULE_NAME) as $field) { 256 | foreach ($field->getFieldgroups() as $fieldgroup) { 257 | $fieldgroup->remove($field); 258 | $fieldgroup->save(); 259 | } 260 | 261 | wire('pages')->get('/')->save(); 262 | 263 | $fields->delete($field); 264 | } 265 | 266 | if ($modules->isInstalled(self::FIELDTYPE_MODULE_NAME)) { 267 | $modules->uninstall(self::FIELDTYPE_MODULE_NAME); 268 | } 269 | 270 | $modules->resetCache(); 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /test/asset/test-custom.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uiii/ProcessWire-FieldtypePDF/9a635df14408ce108e88606ef672f01bbebff75e/test/asset/test-custom.jpg -------------------------------------------------------------------------------- /test/asset/test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uiii/ProcessWire-FieldtypePDF/9a635df14408ce108e88606ef672f01bbebff75e/test/asset/test.jpg -------------------------------------------------------------------------------- /test/asset/test.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uiii/ProcessWire-FieldtypePDF/9a635df14408ce108e88606ef672f01bbebff75e/test/asset/test.pdf -------------------------------------------------------------------------------- /test/bootstrap.php: -------------------------------------------------------------------------------- 1 |