UNPKG

@jsreport/jsreport-pdf-utils

Version:

jsreport extension providing pdf operations like merge or concatenation

793 lines (727 loc) 34.4 kB
import React, { Component, useState } from 'react' import Studio from 'jsreport-studio' import AddHeaderFooterModal from './AddHeaderFooterModal' import AddTOCModal from './AddTOCModal' import AddCoverModal from './AddCoverModal' import styles from './PdfUtilsEditor.css' const EntityRefSelect = Studio.EntityRefSelect const sharedComponents = Studio.sharedComponents const AdvancedMergeModal = (props) => { const { operation: initialOperation, update } = props.options const [operation, setOperation] = useState(initialOperation) const updateOperation = (op) => { update(op) setOperation({ ...operation, ...op }) } return ( <div> <h2>advanced merge configuration</h2> <div className='form-group'> <h3>Merge whole document</h3> <p> render specified template and merge the result into the current pdf. The first page of the template output will be merged into the first page of the current pdf, the second page to the second one and so on. When the option is deselected, the first page of the template output will be merged to all pages of the current pdf. </p> <input type='checkbox' disabled={operation.renderForEveryPage} checked={operation.mergeWholeDocument === true} onChange={(v) => updateOperation({ mergeWholeDocument: v.target.checked, renderForEveryPage: false })} /> </div> <div style={{ marginTop: '1rem' }} className='form-group'> <h3>Render for every page (deprecated)</h3> <p>if true, the operation invokes rendering of the specified template for every pdf page (slow), otherwise it is invoked just once and the single output is merged</p> <input type='checkbox' disabled={operation.mergeWholeDocument} checked={operation.renderForEveryPage === true} onChange={(v) => updateOperation({ renderForEveryPage: v.target.checked, mergeWholeDocument: false })} /> </div> <div style={{ marginTop: '1rem' }} className='form-group'> <h3>Merge to front</h3> <p>if true, the pdf produced by the operation is merged to the front layer, otherwise it is merged to the background</p> <input type='checkbox' checked={operation.mergeToFront === true} onChange={(v) => updateOperation({ mergeToFront: v.target.checked })} /> </div> <div className='button-bar'> <button className='button confirmation' onClick={() => props.close()}>ok</button> </div> </div> ) } class PdfUtilsEditor extends Component { constructor (props) { super(props) this.state = { activeTab: 'operations', customMetadataCreate: null } } addHeaderFooter () { const { entity } = this.props Studio.openModal(AddHeaderFooterModal, { entity }) } addTOC () { const { entity } = this.props Studio.openModal(AddTOCModal, { entity }) } addCover () { const { entity } = this.props Studio.openModal(AddCoverModal, { entity }) } addOperation (entity) { Studio.updateEntity(Object.assign({}, entity, { pdfOperations: [...entity.pdfOperations || [], { type: 'merge', mergeWholeDocument: true }] })) } updateOperation (entity, index, update) { Studio.updateEntity(Object.assign({}, entity, { pdfOperations: entity.pdfOperations.map((o, i) => i === index ? Object.assign({}, o, update) : o) })) } updateMeta (entity, update) { let pdfMeta = entity.pdfMeta || {} pdfMeta = { ...pdfMeta, ...update } Object.keys(pdfMeta).forEach((metaKey) => { if (pdfMeta[metaKey] === '') { delete pdfMeta[metaKey] } }) const keys = Object.keys(pdfMeta) if (keys.length === 0 || keys.every((k) => pdfMeta[k] == null)) { const newEntity = Object.assign({}, entity) newEntity.pdfMeta = null return Studio.updateEntity(newEntity) } Studio.updateEntity(Object.assign({}, entity, { pdfMeta })) } updatePassword (entity, update) { let pdfPassword = entity.pdfPassword || {} pdfPassword = { ...pdfPassword, ...update } Object.keys(pdfPassword).forEach((metaKey) => { if (pdfPassword[metaKey] === '') { delete pdfPassword[metaKey] } }) const keys = Object.keys(pdfPassword) if (keys.length === 0 || keys.every((k) => pdfPassword[k] == null || pdfPassword[k] === false)) { const newEntity = Object.assign({}, entity) newEntity.pdfPassword = null return Studio.updateEntity(newEntity) } Studio.updateEntity(Object.assign({}, entity, { pdfPassword })) } updateSign (entity, update) { let pdfSign = entity.pdfSign || {} pdfSign = { ...pdfSign, ...update } Object.keys(pdfSign).forEach((metaKey) => { if (pdfSign[metaKey] === '') { delete pdfSign[metaKey] } }) const keys = Object.keys(pdfSign) if (keys.length === 0 || keys.every((k) => pdfSign[k] == null)) { const newEntity = Object.assign({}, entity) newEntity.pdfSign = null return Studio.updateEntity(newEntity) } Studio.updateEntity(Object.assign({}, entity, { pdfSign })) } removeOperation (entity, index) { Studio.updateEntity(Object.assign({}, entity, { pdfOperations: entity.pdfOperations.filter((a, i) => i !== index) })) } getExistingCustomMetadata (entity) { let existingCustomMetadata = entity.pdfMeta != null && entity.pdfMeta.custom != null && entity.pdfMeta.custom !== '' ? entity.pdfMeta.custom : '{}' try { existingCustomMetadata = JSON.parse(existingCustomMetadata) } catch (parseError) { existingCustomMetadata = {} } return existingCustomMetadata } removeCustomMetadata (entity, key) { const existingCustomMetadata = this.getExistingCustomMetadata(entity) delete existingCustomMetadata[key] this.updateMeta(entity, { custom: Object.keys(existingCustomMetadata).length > 0 ? JSON.stringify(existingCustomMetadata) : '' }) } updateCustomMetadata (entity, key, value) { if (key === '' || value === '') { return } const existingCustomMetadata = this.getExistingCustomMetadata(entity) existingCustomMetadata[key] = value this.updateMeta(entity, { custom: JSON.stringify(existingCustomMetadata) }) } moveDown (entity, index) { const pdfOperations = [...entity.pdfOperations] const tmp = pdfOperations[index + 1] pdfOperations[index + 1] = pdfOperations[index] pdfOperations[index] = tmp Studio.updateEntity(Object.assign({}, entity, { pdfOperations: pdfOperations })) } moveUp (entity, index) { const pdfOperations = [...entity.pdfOperations] const tmp = pdfOperations[index - 1] pdfOperations[index - 1] = pdfOperations[index] pdfOperations[index] = tmp Studio.updateEntity(Object.assign({}, entity, { pdfOperations: pdfOperations })) } renderOperation (entity, operation, index) { return ( <tr key={index}> <td style={{ minWidth: '170px' }}> <EntityRefSelect headingLabel='Select template' newLabel='New template' filter={(references) => { const templates = references.templates.filter((e) => e.shortid !== entity.shortid && e.recipe.includes('pdf')) return { templates: templates } }} value={operation.templateShortid ? operation.templateShortid : null} onChange={(selected) => this.updateOperation(entity, index, { templateShortid: selected != null && selected.length > 0 ? selected[0].shortid : null })} renderNew={(modalProps) => <sharedComponents.NewTemplateModal {...modalProps} options={{ ...modalProps.options, defaults: { folder: entity.folder }, activateNewTab: false }} />} /> </td> <td> <select value={operation.type} onChange={(v) => this.updateOperation(entity, index, { type: v.target.value })} > <option value='merge'>merge</option> <option value='append'>append</option> <option value='prepend'>prepend</option> </select> </td> <td> {operation.type === 'merge' && <button onClick={() => Studio.openModal(AdvancedMergeModal, { operation: entity.pdfOperations[index], update: (o) => this.updateOperation(entity, index, o) })}>advanced</button>} </td> <td> <input type='checkbox' checked={operation.enabled !== false} onChange={(v) => this.updateOperation(entity, index, { enabled: v.target.checked })} /> </td> <td> <button className='button' style={{ backgroundColor: '#c6c6c6' }} onClick={() => this.removeOperation(entity, index)}><i className='fa fa-times' /></button> </td> <td> {entity.pdfOperations[index - 1] ? <button className='button' style={{ backgroundColor: '#c6c6c6' }} onClick={() => this.moveUp(entity, index)}><i className='fa fa-arrow-up' /></button> : ''} </td> <td> {entity.pdfOperations[index + 1] ? <button className='button' style={{ backgroundColor: '#c6c6c6' }} onClick={() => this.moveDown(entity, index)}><i className='fa fa-arrow-down' /></button> : ''} </td> </tr> ) } renderOperations (entity) { return ( <table className={styles.operationTable}> <thead> <tr> <th>Template</th> <th>Operation</th> <th>Advanced</th> <th>Enabled</th> <th /> <th /> <th /> </tr> </thead> <tbody> {(entity.pdfOperations || []).map((o, i) => this.renderOperation(entity, o, i))} </tbody> </table> ) } renderCustomMetadata (entity) { const existingCustomMetadata = this.getExistingCustomMetadata(entity) const customMetadataList = Object.keys(existingCustomMetadata).reduce((acu, key) => { acu.push({ key, value: existingCustomMetadata[key] }) return acu }, []) let body = null if (customMetadataList.length > 0) { body = customMetadataList.map((item, idx) => ( <tr key={item.key}> <td style={{ textAlign: 'center', paddingLeft: '5px', paddingRight: '10px' }}> <label>{item.key}</label> </td> <td style={{ textAlign: 'center', paddingLeft: '5px' }}> <input type='text' value={item.value} onChange={(v) => this.updateCustomMetadata(entity, item.key, v.target.value)} /> </td> <td> <button className='button' style={{ backgroundColor: '#c6c6c6' }} onClick={() => this.removeCustomMetadata(entity, item.key)}><i className='fa fa-times' /></button> </td> </tr> )) } return ( <table className={styles.operationTable}> <thead> <tr> <th>Key</th> <th>Value</th> <th /> </tr> </thead> <tbody> {body} </tbody> </table> ) } renderCustomMetadataCreate (entity, customMetadataCreate) { const existingCustomMetadata = this.getExistingCustomMetadata(entity) let el if (customMetadataCreate == null) { el = ( <div style={{ marginTop: '1rem', minHeight: '160px' }}> <button className='button confirmation' onClick={() => { const newKeyPrefix = 'key' let targetNewKey = newKeyPrefix let counter = 1 while (existingCustomMetadata[targetNewKey] != null) { counter += 1 targetNewKey = `${newKeyPrefix}${counter}` } this.setState({ customMetadataCreate: { key: targetNewKey, value: 'value' } }) }} > Add custom metadata </button> </div> ) } else { const saveDisabled = customMetadataCreate.key === '' || customMetadataCreate.value === '' el = ( <div style={{ marginTop: '1rem' }}> <hr /> <div className='form-group'> <label>Key</label> <input type='text' value={customMetadataCreate.key} onChange={(v) => this.setState({ customMetadataCreate: { ...customMetadataCreate, key: v.target.value } })} /> </div> <div className='form-group'> <label>Value</label> <input type='text' value={customMetadataCreate.value} onChange={(v) => this.setState({ customMetadataCreate: { ...customMetadataCreate, value: v.target.value } })} /> </div> <div style={{ marginTop: '1rem' }}> <button className={`button confirmation ${saveDisabled ? 'disabled' : ''}`} disabled={saveDisabled} onClick={() => { this.updateCustomMetadata(entity, customMetadataCreate.key, customMetadataCreate.value) this.setState({ customMetadataCreate: null }) }} > {existingCustomMetadata[customMetadataCreate.key] == null || customMetadataCreate.key === '' ? 'Add' : 'Update'} </button> <button className='button confirmation' onClick={() => this.setState({ customMetadataCreate: null })}>Cancel</button> </div> </div> ) } return el } render () { const { activeTab, customMetadataCreate } = this.state const { entity } = this.props const pdfMeta = entity.pdfMeta || {} const pdfA = entity.pdfA || {} const pdfAccessibility = entity.pdfAccessibility || {} const pdfPassword = entity.pdfPassword || {} const pdfSign = entity.pdfSign || {} const pdfCompression = entity.pdfCompression || {} return ( <div className='block custom-editor' style={{ overflowX: 'auto' }}> <h1> <i className='fa fa-file-pdf-o' /> pdf utils configuration </h1> <div> <h2 style={{ marginTop: '0.2rem' }}>quick actions</h2> <div style={{ marginTop: '1rem', marginBottom: '0.8rem' }}> <button className='button confirmation' style={{ marginLeft: 0 }} onClick={() => this.addHeaderFooter()}>Add header/footer</button> <button className='button confirmation' onClick={() => this.addTOC()}>Add Table of Contents</button> <button className='button confirmation' onClick={() => this.addCover()}>Add cover page</button> </div> </div> <div className={styles.separator} /> <div className={styles.tabContainer}> <ul className={styles.tabTitles}> <li className={`${styles.tabTitle} ${activeTab === 'operations' ? styles.active : ''}`} onClick={() => this.setState({ activeTab: 'operations' })} > operations </li> <li className={`${styles.tabTitle} ${activeTab === 'meta' ? styles.active : ''}`} onClick={() => this.setState({ activeTab: 'meta' })} > meta </li> <li className={`${styles.tabTitle} ${activeTab === 'password' ? styles.active : ''}`} onClick={() => this.setState({ activeTab: 'password' })} > password </li> <li className={`${styles.tabTitle} ${activeTab === 'sign' ? styles.active : ''}`} onClick={() => this.setState({ activeTab: 'sign' })} > sign </li> <li className={`${styles.tabTitle} ${activeTab === 'pdfA' ? styles.active : ''}`} onClick={() => this.setState({ activeTab: 'pdfA' })} > pdfA </li> <li className={`${styles.tabTitle} ${activeTab === 'pdfAccessibility' ? styles.active : ''}`} onClick={() => this.setState({ activeTab: 'pdfAccessibility' })} > pdf accessibility </li> <li className={`${styles.tabTitle} ${activeTab === 'pdfCompression' ? styles.active : ''}`} onClick={() => this.setState({ activeTab: 'pdfCompression' })} > compression </li> </ul> <div className={`${styles.tabPanel} ${activeTab === 'operations' ? styles.active : ''}`}> <p style={{ marginTop: '1rem' }}> Use merge/append operations to add dynamic headers or concatenate multiple pdf reports into one. See more docs and examples <a href='https://jsreport.net/learn/pdf-utils'>here</a>. </p> <div style={{ marginTop: '1rem' }}> {this.renderOperations(entity)} </div> <div style={{ marginTop: '1rem' }}> <button className='button confirmation' onClick={() => this.addOperation(entity)}>Add operation</button> </div> </div> <div className={`${styles.tabPanel} ${activeTab === 'meta' ? styles.active : ''}`}> <p style={{ marginTop: '1rem' }}> Add General metadata information to the final PDF. </p> <div style={{ marginTop: '1rem', paddingBottom: '0.5rem' }}> <div className='form-group'> <label>Title</label> <input type='text' value={pdfMeta.title || ''} onChange={(v) => this.updateMeta(entity, { title: v.target.value })} /> </div> <div className='form-group'> <label>Author</label> <input type='text' value={pdfMeta.author || ''} onChange={(v) => this.updateMeta(entity, { author: v.target.value })} /> </div> <div className='form-group'> <label>Subject</label> <input type='text' value={pdfMeta.subject || ''} onChange={(v) => this.updateMeta(entity, { subject: v.target.value })} /> </div> <div className='form-group'> <label>Keywords</label> <input type='text' value={pdfMeta.keywords || ''} onChange={(v) => this.updateMeta(entity, { keywords: v.target.value })} /> </div> <div className='form-group'> <label>Creator</label> <input type='text' value={pdfMeta.creator || ''} onChange={(v) => this.updateMeta(entity, { creator: v.target.value })} /> </div> <div className='form-group'> <label>Producer</label> <input type='text' value={pdfMeta.producer || ''} onChange={(v) => this.updateMeta(entity, { producer: v.target.value })} /> </div> <div className='form-group'> <label>Language</label> <input type='text' value={pdfMeta.language || ''} onChange={(v) => this.updateMeta(entity, { language: v.target.value })} /> </div> </div> <p> Add Custom metadata for the final PDF. </p> <div style={{ marginTop: '1rem' }}> {this.renderCustomMetadata(entity)} </div> {this.renderCustomMetadataCreate(entity, customMetadataCreate)} </div> <div className={`${styles.tabPanel} ${activeTab === 'password' ? styles.active : ''}`}> <div style={{ marginTop: '1rem' }}> Add encryption and access privileges to the final PDF. You can specify either user password, owner password or both passwords. Behavior differs according to passwords you provides: <ul> <li> When only user password is provided, users with user password are able to decrypt the file and have full access to the document. </li> <li> When only owner password is provided, users are able to decrypt and open the document without providing any password, but the access is limited to those operations explicitly permitted. Users with owner password have full access to the document. </li> <li> When both passwords are provided, users with user password are able to decrypt the file but only have limited access to the file according to permission settings. Users with owner password have full access to the document. </li> </ul> </div> <div style={{ marginTop: '1rem', paddingBottom: '0.5rem' }}> <div> <h2>Encryption</h2> <div key='password-field' className='form-group'> <label>User Password</label> <input type='password' autoComplete='off' title='Users will be prompted to enter the password to decrypt the file when opening it' placeholder='user password' value={pdfPassword.password || ''} onChange={(v) => this.updatePassword(entity, { password: v.target.value })} /> </div> </div> <div> <h2>Access privileges</h2> <p> To set access privileges for the PDF, you need to provide an owner password and permission settings. </p> <div key='owner-password-field' className='form-group'> <label>Owner Password</label> <input type='password' autoComplete='off' title='Users with the owner password will always have full access to the PDF (no matter the permission settings)' placeholder='owner password' value={pdfPassword.ownerPassword || ''} onChange={(v) => this.updatePassword(entity, { ownerPassword: v.target.value })} /> </div> <div key='printing-permission-field' className='form-group'> <label>Printing permission</label> <select value={pdfPassword.printing || '-1'} title='Whether printing the file is allowed, and in which resolution the printing can be done' onChange={(v) => this.updatePassword(entity, { printing: v.target.value === '-1' ? null : v.target.value })} > <option key='-1' value='-1'>Not allowed</option> <option key='lowResolution' value='lowResolution' title='Allows the printing in degraded resolution'>Low Resolution</option> <option key='highResolution' value='highResolution' title='Allows the printing in the best resolution'>High Resolution</option> </select> </div> <div key='modify-permission-field' className='form-group'> <label title='Whether modifying the file is allowed'> Modify permission <br /> <input type='checkbox' checked={pdfPassword.modifying === true} onChange={(v) => this.updatePassword(entity, { modifying: v.target.checked })} /> </label> </div> <div key='copy-permission-field' className='form-group'> <label title='Whether copying text or graphics from the file is allowed'> Copy permission <br /> <input type='checkbox' checked={pdfPassword.copying === true} onChange={(v) => this.updatePassword(entity, { copying: v.target.checked })} /> </label> </div> <div key='annotation-permission-field' className='form-group'> <label title='Whether annotating, form filling the file is allowed'> Annotation permission <br /> <input type='checkbox' checked={pdfPassword.annotating === true} onChange={(v) => this.updatePassword(entity, { annotating: v.target.checked })} /> </label> </div> <div key='fillingForms-permission-field' className='form-group'> <label title='Whether form filling and signing the file is allowed'> Filling Forms permission <br /> <input type='checkbox' checked={pdfPassword.fillingForms === true} onChange={(v) => this.updatePassword(entity, { fillingForms: v.target.checked })} /> </label> </div> <div key='contentAccessibility-permission-field' className='form-group'> <label title='Whether copying text from the file for accessibility is allowed'> Content Accessibility permission <br /> <input type='checkbox' checked={pdfPassword.contentAccessibility === true} onChange={(v) => this.updatePassword(entity, { contentAccessibility: v.target.checked })} /> </label> </div> <div key='documentAssembly-permission-field' className='form-group'> <label title='Whether assembling document is allowed'> Assembling Document permission <br /> <input type='checkbox' checked={pdfPassword.documentAssembly === true} onChange={(v) => this.updatePassword(entity, { documentAssembly: v.target.checked })} /> </label> </div> </div> </div> </div> <div className={`${styles.tabPanel} ${activeTab === 'sign' ? styles.active : ''}`}> <p style={{ marginTop: '1rem' }}> Add a digital signature to the final PDF. </p> <div style={{ marginTop: '1rem', paddingBottom: '0.5rem' }}> <div className='form-group'> <label>Select certificate (asset)</label> <EntityRefSelect headingLabel='Select certificate' newLabel='New certificate asset' value={pdfSign.certificateAssetShortid || ''} onChange={(selected) => this.updateSign(entity, { certificateAssetShortid: selected.length > 0 ? selected[0].shortid : null })} filter={(references) => ({ data: references.assets })} renderNew={(modalProps) => <sharedComponents.NewAssetModal {...modalProps} options={{ ...modalProps.options, defaults: { folder: entity.folder }, activateNewTab: false }} />} /> </div> <div className='form-group'> <label>Sign Reason filled to pdf</label> <input type='text' placeholder='signed...' value={pdfSign.reason} onChange={(v) => this.updateSign(entity, { reason: v.target.value })} /> </div> </div> </div> <div className={`${styles.tabPanel} ${activeTab === 'pdfA' ? styles.active : ''}`}> <p style={{ marginTop: '1rem' }}> Produce output complying with PDF/A-1B standard (beta) </p> <div style={{ marginTop: '1rem', paddingBottom: '0.5rem' }}> <div className='form-group'> <label> Enabled <br /> <input type='checkbox' checked={pdfA.enabled === true} onChange={(v) => Studio.updateEntity(Object.assign({}, entity, { pdfA: { enabled: v.target.checked } }))} /> </label> </div> </div> </div> <div className={`${styles.tabPanel} ${activeTab === 'pdfAccessibility' ? styles.active : ''}`}> <h2 style={{ marginTop: '1rem' }}> copy tags during operations (beta) </h2> <div style={{ marginTop: '1rem', paddingBottom: '0.5rem' }}> <div className='form-group'> <label> Enabled <br /> <input type='checkbox' checked={pdfAccessibility.enabled === true} onChange={(v) => Studio.updateEntity(Object.assign({}, entity, { pdfAccessibility: { ...entity.pdfAccessibility, enabled: v.target.checked } }))} /> </label> </div> </div> <h2 style={{ marginTop: '1rem' }}> pdf/UA (beta) </h2> <p>tasks for the template developer: <ul> <li>html img needs to have the alt attribute</li> <li>the pdf utils meta needs to have filled language</li> <li>"copy tags" needs to be selected</li> </ul> </p> <div style={{ marginTop: '1rem', paddingBottom: '0.5rem' }}> <div className='form-group'> <label> Enabled <br /> <input type='checkbox' checked={pdfAccessibility.pdfUA === true} onChange={(v) => Studio.updateEntity(Object.assign({}, entity, { pdfAccessibility: { ...entity.pdfAccessibility, pdfUA: v.target.checked } }))} /> </label> </div> </div> </div> <div className={`${styles.tabPanel} ${activeTab === 'pdfCompression' ? styles.active : ''}`}> <h2 style={{ marginTop: '1rem' }}> compression (beta) </h2> <div style={{ marginTop: '1rem', paddingBottom: '0.5rem' }}> <div className='form-group'> <label> Enabled <br /> <input type='checkbox' checked={pdfCompression.enabled === true} onChange={(v) => Studio.updateEntity(Object.assign({}, entity, { pdfCompression: { ...entity.pdfCompression, enabled: v.target.checked } }))} /> </label> </div> <div className='form-group'> <label>remove accessibility</label> <input type='checkbox' checked={pdfCompression.removeAccessibility !== false} onChange={(v) => Studio.updateEntity(Object.assign({}, entity, { pdfCompression: { ...entity.pdfCompression, removeAccessibility: v.target.checked } }))} /> </div> <div className='form-group'> <label>recompress streams</label> <input type='checkbox' checked={pdfCompression.recompressStreams !== false} onChange={(v) => Studio.updateEntity(Object.assign({}, entity, { pdfCompression: { ...entity.pdfCompression, recompressStreams: v.target.checked } }))} /> </div> <div className='form-group'> <label>use object streams</label> <input type='checkbox' checked={pdfCompression.useObjectStreams !== false} onChange={(v) => Studio.updateEntity(Object.assign({}, entity, { pdfCompression: { ...entity.pdfCompression, useObjectStreams: v.target.checked } }))} /> </div> <div className='form-group'> <label>remove effects</label> <input type='checkbox' checked={pdfCompression.removeEffects !== false} onChange={(v) => Studio.updateEntity(Object.assign({}, entity, { pdfCompression: { ...entity.pdfCompression, removeEffects: v.target.checked } }))} /> </div> <div className='form-group'> <label>convert images to jpeg</label> <input type='checkbox' checked={pdfCompression.convertImagesToJpeg !== false} onChange={(v) => Studio.updateEntity(Object.assign({}, entity, { pdfCompression: { ...entity.pdfCompression, convertImagesToJpeg: v.target.checked } }))} /> </div> <div className='form-group'> <label>JPEG quality</label> <input type='number' min='0' max='100' placeholder='60' value={pdfCompression.jpegQuality || ''} onChange={(v) => Studio.updateEntity(Object.assign({}, entity, { pdfCompression: { ...entity.pdfCompression, jpegQuality: parseInt(v.target.value, 10) } }))} /> </div> </div> </div> </div> </div> ) } } export default PdfUtilsEditor