diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index 4c6eab9..3063636 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -18,7 +18,7 @@ object Versions { // Latest Version: // Visit https://central.sonatype.com and search for: g:org.jodconverter a:jodconverter-core - const val jodConverter = "4.4.7" + const val jodConverter = "4.4.9" // Latest Version: // Visit https://central.sonatype.com and search for: g:commons-fileupload a:commons-fileupload diff --git a/samples/spring-boot-rest/src/main/java/org/jodconverter/sample/rest/AcceptAllTrackedChangesFilter.java b/samples/spring-boot-rest/src/main/java/org/jodconverter/sample/rest/AcceptAllTrackedChangesFilter.java new file mode 100644 index 0000000..1fd866e --- /dev/null +++ b/samples/spring-boot-rest/src/main/java/org/jodconverter/sample/rest/AcceptAllTrackedChangesFilter.java @@ -0,0 +1,88 @@ +package org.jodconverter.sample.rest; + +import com.sun.star.beans.PropertyValue; +import com.sun.star.frame.XController; +import com.sun.star.frame.XDispatchHelper; +import com.sun.star.frame.XDispatchProvider; +import com.sun.star.frame.XFrame; +import com.sun.star.frame.XModel; +import com.sun.star.lang.XComponent; +import com.sun.star.uno.XComponentContext; +import org.jodconverter.core.office.OfficeContext; +import org.jodconverter.local.filter.Filter; +import org.jodconverter.local.filter.FilterChain; +import org.jodconverter.local.office.LocalOfficeContext; +import org.jodconverter.local.office.utils.Lo; +import org.jodconverter.local.office.utils.Write; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A {@link Filter} that accepts all tracked changes (redlines) in a Writer document before it is + * passed further down the conversion chain (e.g. exported to PDF). + * + *

By default LibreOffice exports a document with its tracked changes preserved, so the resulting + * PDF may still contain insertion/deletion markup. This filter applies all revisions in memory by + * dispatching the {@code .uno:AcceptAllTrackedChanges} command, producing the clean "final" view. + * + *

Only Writer (text) documents are affected; other document types pass through untouched. + */ +public class AcceptAllTrackedChangesFilter implements Filter { + + private static final Logger LOGGER = + LoggerFactory.getLogger(AcceptAllTrackedChangesFilter.class); + + private static final String ACCEPT_ALL_TRACKED_CHANGES = ".uno:AcceptAllTrackedChanges"; + + @Override + public void doFilter( + final OfficeContext context, final XComponent document, final FilterChain chain) + throws Exception { + + if (Write.isText(document)) { + LOGGER.debug("Applying AcceptAllTrackedChangesFilter to text document"); + acceptAllTrackedChanges(context, document); + } else { + LOGGER.debug("Skipping AcceptAllTrackedChangesFilter (document is not a text document)"); + } + + // Continue the conversion chain. + chain.doFilter(context, document); + } + + private static void acceptAllTrackedChanges( + final OfficeContext context, final XComponent document) { + + final XComponentContext componentContext = + ((LocalOfficeContext) context).getComponentContext(); + if (componentContext == null) { + LOGGER.warn("No component context available; cannot accept tracked changes"); + return; + } + + final XModel model = Lo.qi(XModel.class, document); + final XController controller = model.getCurrentController(); + if (controller == null) { + LOGGER.warn("No controller available; cannot accept tracked changes"); + return; + } + + final XFrame frame = controller.getFrame(); + if (frame == null) { + LOGGER.warn("No frame available; cannot accept tracked changes"); + return; + } + + final XDispatchProvider dispatchProvider = Lo.qi(XDispatchProvider.class, frame); + final XDispatchHelper dispatchHelper = + Lo.createInstance( + componentContext, XDispatchHelper.class, "com.sun.star.frame.DispatchHelper"); + if (dispatchHelper == null) { + LOGGER.warn("Could not create DispatchHelper; cannot accept tracked changes"); + return; + } + + dispatchHelper.executeDispatch( + dispatchProvider, ACCEPT_ALL_TRACKED_CHANGES, "", 0, new PropertyValue[0]); + } +} diff --git a/samples/spring-boot-rest/src/main/java/org/jodconverter/sample/rest/ConverterController.java b/samples/spring-boot-rest/src/main/java/org/jodconverter/sample/rest/ConverterController.java index 8e1b617..603a63b 100644 --- a/samples/spring-boot-rest/src/main/java/org/jodconverter/sample/rest/ConverterController.java +++ b/samples/spring-boot-rest/src/main/java/org/jodconverter/sample/rest/ConverterController.java @@ -15,6 +15,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -49,6 +50,15 @@ public class ConverterController { @Autowired private ParameterDecoder parameterDecoder; + /** + * Default for whether all tracked changes should be accepted before conversion. Applied when a + * request does not explicitly set the {@code acceptTrackedChanges} parameter. Configurable via + * the {@code jodconverter.accept-tracked-changes} property (env {@code + * JODCONVERTER_ACCEPT_TRACKED_CHANGES}). + */ + @Value("${jodconverter.accept-tracked-changes:false}") + private boolean acceptTrackedChangesByDefault; + /** * Creates a new controller. * @@ -82,11 +92,17 @@ public ConverterController(final OfficeManager officeManager) { description = "The document format to convert the input document to.", required = true) @RequestParam(name = "format") final String convertToFormat, + @Parameter( + description = + "Whether to accept all tracked changes before converting. Defaults to the" + + " server configuration when omitted.") + @RequestParam(name = "acceptTrackedChanges", required = false) + final Boolean acceptTrackedChanges, @Parameter(description = "The custom options to apply to the conversion.") @RequestParam(required = false) final Map parameters) { LOGGER.debug("convertUsingRequestParam > Converting file to {}", convertToFormat); - return convert(inputFile, convertToFormat, parameters); + return convert(inputFile, convertToFormat, acceptTrackedChanges, parameters); } @Operation( @@ -112,16 +128,23 @@ public ConverterController(final OfficeManager officeManager) { description = "The document format to convert the input document to.", required = true) @PathVariable(name = "format") final String convertToFormat, + @Parameter( + description = + "Whether to accept all tracked changes before converting. Defaults to the" + + " server configuration when omitted.") + @RequestParam(name = "acceptTrackedChanges", required = false) + final Boolean acceptTrackedChanges, @Parameter(description = "The custom options to apply to the conversion.") @RequestParam(required = false) final Map parameters) { LOGGER.debug("convertUsingPathVariable > Converting file to {}", convertToFormat); - return convert(inputFile, convertToFormat, parameters); + return convert(inputFile, convertToFormat, acceptTrackedChanges, parameters); } private ResponseEntity convert( final MultipartFile inputFile, final String outputFormat, + final Boolean acceptTrackedChanges, final Map parameters) { if (inputFile.isEmpty()) { @@ -145,13 +168,21 @@ private ResponseEntity convert( final Map storeProperties = new HashMap<>(); parameterDecoder.decodeParameters(parameters, loadProperties, storeProperties); - // Create a converter with the properties. - final DocumentConverter converter = + // Create a converter with the properties, optionally accepting all tracked changes + // (redlines) before the document is exported. + final LocalConverter.Builder converterBuilder = LocalConverter.builder() .officeManager(officeManager) .loadProperties(loadProperties) - .storeProperties(storeProperties) - .build(); + .storeProperties(storeProperties); + + final boolean acceptChanges = + acceptTrackedChanges != null ? acceptTrackedChanges : acceptTrackedChangesByDefault; + if (acceptChanges) { + converterBuilder.filterChain(new AcceptAllTrackedChangesFilter()); + } + + final DocumentConverter converter = converterBuilder.build(); // Convert... converter.convert(inputFile.getInputStream()).to(baos).as(targetFormat).execute(); diff --git a/samples/spring-boot-rest/src/main/resources/application.yml b/samples/spring-boot-rest/src/main/resources/application.yml index 7a72ead..96ca893 100644 --- a/samples/spring-boot-rest/src/main/resources/application.yml +++ b/samples/spring-boot-rest/src/main/resources/application.yml @@ -20,6 +20,10 @@ springdoc: path: /swagger-ui.html jodconverter: + # Accept all tracked changes (redlines) before converting a document (e.g. docx -> pdf). + # Can be overridden per request with the `acceptTrackedChanges` request parameter. + # Set JODCONVERTER_ACCEPT_TRACKED_CHANGES=true to enable globally. + accept-tracked-changes: ${JODCONVERTER_ACCEPT_TRACKED_CHANGES:false} local: enabled: true port-numbers: 2002,2003