View Javadoc
1   package com.acumenvelocity.ath.filters.pdf;
2   
3   import java.io.File;
4   import java.io.FileInputStream;
5   import java.io.FileOutputStream;
6   import java.io.IOException;
7   import java.io.InputStream;
8   import java.io.OutputStream;
9   
10  import com.acumenvelocity.ath.common.AthUtil;
11  import com.acumenvelocity.ath.common.Log;
12  import com.acumenvelocity.ath.common.PdfUtil;
13  
14  import net.sf.okapi.common.Event;
15  import net.sf.okapi.common.IParameters;
16  import net.sf.okapi.common.LocaleId;
17  import net.sf.okapi.common.StreamUtil;
18  import net.sf.okapi.common.encoder.EncoderManager;
19  import net.sf.okapi.common.exceptions.OkapiIOException;
20  import net.sf.okapi.common.filterwriter.IFilterWriter;
21  import net.sf.okapi.common.skeleton.ISkeletonWriter;
22  import net.sf.okapi.filters.openxml.OpenXMLFilterWriter;
23  
24  /**
25   * Filter writer for PDF files that merges translations back into the original format.
26   * Uses Adobe PDF Services to convert DOCX back to PDF.
27   */
28  public class AthPdfFilterWriter implements IFilterWriter {
29    
30    private OutputStream output;
31    private LocaleId targetLocale;
32    private String defaultEncoding;
33    private IParameters parameters;
34    private boolean isStarted;
35    private boolean isCancelled;
36    private boolean ownsOutputStream;
37  
38    private OpenXMLFilterWriter docxWriter;
39    private File tempDocxFile;
40    private OutputStream docxOutputStream;
41  
42    public AthPdfFilterWriter(OpenXMLFilterWriter docxWriter) {
43      this.docxWriter = docxWriter;
44      this.isStarted = false;
45      this.isCancelled = false;
46      this.ownsOutputStream = false;
47    }
48  
49    @Override
50    public String getName() {
51      return "AthPdfFilterWriter";
52    }
53  
54    @Override
55    public void setOptions(LocaleId locale, String defaultEncoding) {
56      this.targetLocale = locale;
57      this.defaultEncoding = defaultEncoding;
58    }
59  
60    @Override
61    public void setOutput(String path) {
62      try {
63        File file = new File(path);
64        File parent = file.getParentFile();
65        
66        if (parent != null && !parent.exists()) {
67          parent.mkdirs();
68        }
69        
70        this.output = new FileOutputStream(file);
71        this.ownsOutputStream = true;
72        
73      } catch (IOException e) {
74        throw new OkapiIOException("Error creating output file: " + path, e);
75      }
76    }
77  
78    @Override
79    public void setOutput(OutputStream output) {
80      this.output = output;
81      this.ownsOutputStream = false;
82    }
83  
84    @Override
85    public Event handleEvent(Event event) {
86      if (isCancelled) {
87        return event;
88      }
89  
90      switch (event.getEventType()) {
91      case START_DOCUMENT:
92        handleStartDocument(event);
93        break;
94  
95      case END_DOCUMENT:
96        handleEndDocument(event);
97        break;
98  
99      case TEXT_UNIT:
100     case DOCUMENT_PART:
101     case START_SUBDOCUMENT:
102     case END_SUBDOCUMENT:
103     case START_GROUP:
104     case END_GROUP:
105     case START_SUBFILTER:
106     case END_SUBFILTER:
107       // Forward all events to DOCX writer
108       if (docxWriter != null) {
109         docxWriter.handleEvent(event);
110       }
111       
112       break;
113 
114     default:
115       break;
116     }
117 
118     return event;
119   }
120 
121   private void handleStartDocument(Event event) {
122     isStarted = true;
123     isCancelled = false;
124 
125     Log.info(getClass(), "=== PDF Filter Writer Started ===");
126     Log.info(getClass(), "Target locale: " + targetLocale);
127 
128     try {
129       // Create temporary file for DOCX
130       tempDocxFile = AthUtil.createTempFile();      
131       docxOutputStream = new FileOutputStream(tempDocxFile);
132 
133       // Create DOCX writer
134 
135       if (docxWriter == null) {
136         throw new OkapiIOException("OpenXML filter writer not available");
137       }
138 
139       docxWriter.setOptions(targetLocale, defaultEncoding);
140       docxWriter.setOutput(docxOutputStream);
141 
142       // Forward START_DOCUMENT to DOCX writer
143       docxWriter.handleEvent(event);
144 
145       Log.info(getClass(), "DOCX writer initialized with temp file: " + tempDocxFile.getAbsolutePath());
146 
147     } catch (Exception e) {
148       cleanupTempFile();
149       throw new OkapiIOException("Error initializing DOCX writer", e);
150     }
151   }
152 
153   private void handleEndDocument(Event event) {
154     if (!isStarted || isCancelled || output == null) {
155       return;
156     }
157 
158     InputStream docxInputStream = null;
159     
160     try {
161       Log.info(getClass(), "\n=== Writing PDF ===");
162 
163       // Forward END_DOCUMENT to DOCX writer to finalize DOCX
164       if (docxWriter != null) {
165         docxWriter.handleEvent(event);
166         docxWriter.close();
167       }
168 
169       // Close the DOCX output stream
170       if (docxOutputStream != null) {
171         docxOutputStream.close();
172         docxOutputStream = null;
173       }
174 
175       Log.info(getClass(), "DOCX generation completed. Size: " + tempDocxFile.length() + " bytes");
176 
177       // Open input stream from temp file
178       docxInputStream = new FileInputStream(tempDocxFile);
179 
180       // Convert DOCX to PDF using Adobe PDF Services
181       InputStream pdfInputStream = PdfUtil.convertDocxToPdf(docxInputStream, targetLocale);
182       
183       // Copy PDF to output stream using utility method
184       StreamUtil.copy(pdfInputStream, output);
185 
186       Log.info(getClass(), "PDF written successfully!");
187 
188     } catch (Exception e) {
189       Log.error(getClass(), "Error writing PDF", e);
190       throw new OkapiIOException("Error writing translated PDF", e);
191       
192     } finally {
193       if (docxInputStream != null) {
194         try {
195           docxInputStream.close();
196           
197         } catch (IOException e) {
198           // Ignore
199         }
200       }
201       
202       cleanupTempFile();
203     }
204   }
205 
206   private void cleanupTempFile() {
207     if (tempDocxFile != null && tempDocxFile.exists()) {
208       try {
209         tempDocxFile.delete();
210         Log.info(getClass(), "Temporary DOCX file deleted");
211         
212       } catch (Exception e) {
213         Log.error(getClass(), "Failed to delete temp file: " + tempDocxFile.getAbsolutePath(), e);
214       }
215       
216       tempDocxFile = null;
217     }
218   }
219 
220   @Override
221   public void close() {
222     Log.info(getClass(), "=== PDF Filter Writer Closed ===\n");
223 
224     isStarted = false;
225     isCancelled = false;
226 
227     if (docxWriter != null) {
228       try {
229         docxWriter.close();
230         
231       } catch (Exception e) {
232         Log.error(getClass(), "Error closing DOCX writer", e);
233       }
234       
235       docxWriter = null;
236     }
237 
238     if (docxOutputStream != null) {
239       try {
240         docxOutputStream.close();
241         
242       } catch (IOException e) {
243         // Ignore
244       }
245       
246       docxOutputStream = null;
247     }
248 
249     cleanupTempFile();
250 
251     if (output != null && ownsOutputStream) {
252       try {
253         output.close();
254         
255       } catch (IOException e) {
256         throw new OkapiIOException("Error closing output stream", e);
257       }
258     }
259 
260     output = null;
261   }
262 
263   @Override
264   public IParameters getParameters() {
265     return parameters;
266   }
267 
268   @Override
269   public void setParameters(IParameters params) {
270     this.parameters = params;
271   }
272 
273   @Override
274   public void cancel() {
275     isCancelled = true;
276 
277     if (docxWriter != null) {
278       docxWriter.cancel();
279     }
280 
281     cleanupTempFile();
282     Log.info(getClass(), "PDF Filter Writer cancelled");
283   }
284 
285   @Override
286   public EncoderManager getEncoderManager() {
287     return null;
288   }
289 
290   @Override
291   public ISkeletonWriter getSkeletonWriter() {
292     return null;
293   }
294 
295   public LocaleId getTargetLocale() {
296     return targetLocale;
297   }
298 
299   public String getDefaultEncoding() {
300     return defaultEncoding;
301   }
302 
303   public boolean isCancelled() {
304     return isCancelled;
305   }
306 }