1 package com.acumenvelocity.ath.pagematcher;
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.UncheckedIOException;
8 import java.nio.file.Files;
9 import java.nio.file.Path;
10 import java.nio.file.Paths;
11 import java.nio.file.StandardCopyOption;
12 import java.util.concurrent.atomic.AtomicInteger;
13 import java.util.zip.ZipEntry;
14 import java.util.zip.ZipInputStream;
15 import java.util.zip.ZipOutputStream;
16
17 import javax.xml.parsers.DocumentBuilder;
18 import javax.xml.parsers.DocumentBuilderFactory;
19 import javax.xml.transform.OutputKeys;
20 import javax.xml.transform.Transformer;
21 import javax.xml.transform.TransformerFactory;
22 import javax.xml.transform.dom.DOMSource;
23 import javax.xml.transform.stream.StreamResult;
24
25 import org.apache.pdfbox.Loader;
26 import org.apache.pdfbox.pdmodel.PDDocument;
27 import org.jodconverter.local.LocalConverter;
28 import org.jodconverter.local.office.LocalOfficeManager;
29 import org.w3c.dom.Document;
30 import org.w3c.dom.Element;
31 import org.w3c.dom.NodeList;
32
33 public class DocxPageMatcherTest {
34
35 private static final String WORD_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
36 private static final LocalOfficeManager officeManager = LocalOfficeManager.builder().build();
37
38 public static void main(String[] args) throws Exception {
39 if (args.length != 2) {
40 System.out.println("Usage: java DocxPageMatcher <original.docx> <translation.docx>");
41 return;
42 }
43
44 String originalPath = args[0];
45 String translationPath = args[1];
46
47 startOfficeManager();
48 try {
49
50
51
52
53
54
55
56
57
58
59
60
61
62 System.out.println("Starting page count matching process...");
63 matchPageCount(originalPath, translationPath);
64
65 } finally {
66 stopOfficeManager();
67 }
68 }
69
70
71
72
73
74
75
76 public static void matchPageCount(String originalDocxPath, String translationDocxPath)
77 throws Exception {
78 int originalPages = getPageCount(originalDocxPath);
79 int translationPages = getPageCount(translationDocxPath);
80
81 System.out.printf("Original pages: %d%n", originalPages);
82 System.out.printf("Translation pages: %d%n", translationPages);
83
84 if (translationPages <= originalPages) {
85 System.out.println("Translation page count is acceptable. No changes needed.");
86 return;
87 }
88
89
90 System.out.println("\nPhase 1: Attempting to adjust styles.xml...");
91 boolean stylesSuccess = shrinkFontToFitPages(translationDocxPath, originalPages, true);
92
93 if (!stylesSuccess) {
94
95 System.out.println("\nPhase 2: Adjusting document.xml...");
96 shrinkFontToFitPages(translationDocxPath, originalPages, false);
97 }
98
99 int finalPages = getPageCount(translationDocxPath);
100 System.out.printf("\nFinal result: %d pages (target: %d)%n", finalPages, originalPages);
101 }
102
103 private static boolean shrinkFontToFitPages(String docxPath, int targetPages, boolean useStyles)
104 throws Exception {
105
106 String backupPath = docxPath + ".backup";
107
108
109 Files.copy(Paths.get(docxPath), Paths.get(backupPath), StandardCopyOption.REPLACE_EXISTING);
110
111 int currentPages = getPageCount(docxPath);
112 System.out.printf("Initial page count: %d (target: %d)%n", currentPages, targetPages);
113
114 if (currentPages <= targetPages) {
115 System.out.println("No adjustment needed – page count already acceptable.");
116 Files.delete(Paths.get(backupPath));
117 return true;
118 }
119
120 int iteration = 0;
121 double totalDelta = 0.0;
122
123 while (currentPages > targetPages) {
124 iteration++;
125 double deltaThisStep = -1.0;
126 totalDelta += deltaThisStep;
127
128 System.out.printf("Iteration %d: Applying delta=%.1fpt (total delta=%.1fpt)%n",
129 iteration, deltaThisStep, totalDelta);
130
131
132 Files.copy(Paths.get(backupPath), Paths.get(docxPath), StandardCopyOption.REPLACE_EXISTING);
133
134
135 if (useStyles) {
136 adjustStylesFontSize(docxPath, totalDelta);
137
138 } else {
139 adjustDocumentFontSize(docxPath, totalDelta);
140 }
141
142 currentPages = getPageCount(docxPath);
143 System.out.printf(" Result: %d pages (target: %d)%n", currentPages, targetPages);
144
145 if (currentPages <= targetPages) {
146 System.out.println(" ✓ Target page count achieved or exceeded!");
147 Files.delete(Paths.get(backupPath));
148 return true;
149 }
150
151
152 if (iteration >= 10) {
153 System.out.println(" Maximum iterations reached. Stopping to avoid over-shrinking.");
154 break;
155 }
156 }
157
158
159 if (currentPages > targetPages) {
160 System.out.println("Could not reach target page count. Applying the best result so far.");
161
162 }
163
164 Files.delete(Paths.get(backupPath));
165 return currentPages <= targetPages;
166 }
167
168 private static void adjustStylesFontSize(String docxPath, double delta) throws Exception {
169 Path tempDir = Files.createTempDirectory("docx_");
170 unzip(docxPath, tempDir.toString());
171
172 Path stylesPath = tempDir.resolve("word/styles.xml");
173 if (!Files.exists(stylesPath)) {
174 System.out.println("styles.xml not found");
175 return;
176 }
177
178 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
179 factory.setNamespaceAware(true);
180 DocumentBuilder builder = factory.newDocumentBuilder();
181 Document doc = builder.parse(stylesPath.toFile());
182
183 NodeList szNodes = doc.getElementsByTagNameNS(WORD_NS, "sz");
184 NodeList szCsNodes = doc.getElementsByTagNameNS(WORD_NS, "szCs");
185
186 AtomicInteger modified = new AtomicInteger(0);
187
188 java.util.function.Consumer<Element> adjustFontSize = szElement -> {
189 String valAttr = szElement.getAttribute("w:val");
190 if (!valAttr.isEmpty()) {
191 try {
192 double currentSize = Double.parseDouble(valAttr) / 2.0;
193 double newSize = Math.max(1.0, currentSize + delta);
194 szElement.setAttribute("w:val", String.valueOf((int) (newSize * 2)));
195 modified.incrementAndGet();
196 } catch (NumberFormatException e) {
197
198 }
199 }
200 };
201
202
203 for (int i = 0; i < szNodes.getLength(); i++) {
204 adjustFontSize.accept((Element) szNodes.item(i));
205 }
206
207
208 for (int i = 0; i < szCsNodes.getLength(); i++) {
209 adjustFontSize.accept((Element) szCsNodes.item(i));
210 }
211
212 System.out.printf(" Modified %d font size entries (sz + szCs) in styles.xml%n",
213 modified.get());
214
215 TransformerFactory transformerFactory = TransformerFactory.newInstance();
216 Transformer transformer = transformerFactory.newTransformer();
217 transformer.setOutputProperty(OutputKeys.INDENT, "yes");
218 transformer.transform(new DOMSource(doc), new StreamResult(stylesPath.toFile()));
219
220 zip(tempDir.toString(), docxPath);
221 deleteDirectory(tempDir.toFile());
222 }
223
224 private static void adjustDocumentFontSize(String docxPath, double delta) throws Exception {
225 Path tempDir = Files.createTempDirectory("docx_");
226 unzip(docxPath, tempDir.toString());
227
228 Path documentPath = tempDir.resolve("word/document.xml");
229 if (!Files.exists(documentPath)) {
230 System.out.println("document.xml not found");
231 return;
232 }
233
234 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
235 factory.setNamespaceAware(true);
236 DocumentBuilder builder = factory.newDocumentBuilder();
237 Document doc = builder.parse(documentPath.toFile());
238
239 NodeList szNodes = doc.getElementsByTagNameNS(WORD_NS, "sz");
240 NodeList szCsNodes = doc.getElementsByTagNameNS(WORD_NS, "szCs");
241
242 AtomicInteger modified = new AtomicInteger(0);
243
244 java.util.function.Consumer<Element> adjustFontSize = szElement -> {
245 String valAttr = szElement.getAttribute("w:val");
246 if (!valAttr.isEmpty()) {
247 try {
248 double currentSize = Double.parseDouble(valAttr) / 2.0;
249 double newSize = Math.max(1.0, currentSize + delta);
250
251 szElement.setAttribute("w:val", String.valueOf((int) (newSize * 2)));
252 modified.incrementAndGet();
253
254 } catch (NumberFormatException e) {
255
256 }
257 }
258 };
259
260
261 for (int i = 0; i < szNodes.getLength(); i++) {
262 adjustFontSize.accept((Element) szNodes.item(i));
263 }
264
265
266 for (int i = 0; i < szCsNodes.getLength(); i++) {
267 adjustFontSize.accept((Element) szCsNodes.item(i));
268 }
269
270 System.out.printf("Modified %d font size entries (sz + szCs) in document.xml%n",
271 modified.get());
272
273 TransformerFactory transformerFactory = TransformerFactory.newInstance();
274 Transformer transformer = transformerFactory.newTransformer();
275 transformer.setOutputProperty(OutputKeys.INDENT, "yes");
276 transformer.transform(new DOMSource(doc), new StreamResult(documentPath.toFile()));
277
278 zip(tempDir.toString(), docxPath);
279 deleteDirectory(tempDir.toFile());
280 }
281
282 private static int getPageCount(String docxPath) throws Exception {
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297 return getAccuratePageCount(docxPath);
298 }
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326 public static void startOfficeManager() {
327 try {
328 officeManager.start();
329 System.out.println("LibreOffice manager started successfully");
330 } catch (Exception e) {
331
332 e.printStackTrace();
333 }
334 }
335
336 public static void stopOfficeManager() {
337 try {
338 officeManager.stop();
339 System.out.println("LibreOffice manager stopped successfully");
340
341 } catch (Exception e) {
342
343 e.printStackTrace();
344 }
345 }
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394 private static int getAccuratePageCount(String docxPath) throws Exception {
395 File inputFile = new File(docxPath);
396 File tempPdf = File.createTempFile("pagecount_", ".pdf");
397
398 try {
399
400 LocalConverter.builder()
401 .officeManager(officeManager)
402 .build()
403 .convert(inputFile)
404 .to(tempPdf)
405 .execute();
406
407 try (PDDocument pdfDoc = Loader.loadPDF(tempPdf)) {
408 return pdfDoc.getNumberOfPages();
409 }
410
411 } finally {
412 if (tempPdf.exists() && !tempPdf.delete()) {
413 tempPdf.deleteOnExit();
414 }
415 }
416 }
417
418 private static void unzip(String zipFilePath, String destDir) throws IOException {
419 File dir = new File(destDir);
420 if (!dir.exists())
421 dir.mkdirs();
422
423 try (ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFilePath))) {
424 ZipEntry entry;
425 while ((entry = zis.getNextEntry()) != null) {
426 File file = new File(destDir, entry.getName());
427 if (entry.isDirectory()) {
428 file.mkdirs();
429 } else {
430 file.getParentFile().mkdirs();
431 try (FileOutputStream fos = new FileOutputStream(file)) {
432 byte[] buffer = new byte[4096];
433 int len;
434 while ((len = zis.read(buffer)) > 0) {
435 fos.write(buffer, 0, len);
436 }
437 }
438 }
439 zis.closeEntry();
440 }
441 }
442 }
443
444 private static void zip(String sourceDirPath, String zipFilePath) throws IOException {
445 Path source = Paths.get(sourceDirPath);
446 try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFilePath))) {
447 Files.walk(source).filter(path -> !Files.isDirectory(path)).forEach(path -> {
448 ZipEntry entry = new ZipEntry(source.relativize(path).toString().replace("\\", "/"));
449 try {
450 zos.putNextEntry(entry);
451 Files.copy(path, zos);
452 zos.closeEntry();
453 } catch (IOException e) {
454 throw new UncheckedIOException(e);
455 }
456 });
457 }
458 }
459
460 private static void deleteDirectory(File dir) {
461 File[] files = dir.listFiles();
462 if (files != null) {
463 for (File file : files) {
464 if (file.isDirectory()) {
465 deleteDirectory(file);
466 } else {
467 file.delete();
468 }
469 }
470 }
471 dir.delete();
472 }
473 }