You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

HyphenationTree.java 23KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641
  1. /*
  2. * Licensed to the Apache Software Foundation (ASF) under one or more
  3. * contributor license agreements. See the NOTICE file distributed with
  4. * this work for additional information regarding copyright ownership.
  5. * The ASF licenses this file to You under the Apache License, Version 2.0
  6. * (the "License"); you may not use this file except in compliance with
  7. * the License. You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. /* $Id$ */
  18. package org.apache.fop.hyphenation;
  19. import java.io.BufferedReader;
  20. import java.io.File;
  21. import java.io.FileInputStream;
  22. import java.io.FileOutputStream;
  23. import java.io.FileReader;
  24. import java.io.IOException;
  25. import java.io.ObjectInputStream;
  26. import java.io.ObjectOutputStream;
  27. import java.io.Serializable;
  28. import java.net.MalformedURLException;
  29. import java.util.ArrayList;
  30. import java.util.HashMap;
  31. import org.xml.sax.InputSource;
  32. /**
  33. * <p>This tree structure stores the hyphenation patterns in an efficient
  34. * way for fast lookup. It provides the provides the method to
  35. * hyphenate a word.</p>
  36. *
  37. * <p>This work was authored by Carlos Villegas (cav@uniscope.co.jp).</p>
  38. */
  39. public class HyphenationTree extends TernaryTree
  40. implements PatternConsumer, Serializable {
  41. private static final long serialVersionUID = -7842107987915665573L;
  42. /**
  43. * value space: stores the interletter values
  44. */
  45. protected ByteVector vspace;
  46. /**
  47. * This map stores hyphenation exceptions
  48. */
  49. protected HashMap stoplist;
  50. /**
  51. * This map stores the character classes
  52. */
  53. protected TernaryTree classmap;
  54. /**
  55. * Temporary map to store interletter values on pattern loading.
  56. */
  57. private transient TernaryTree ivalues;
  58. /** Default constructor. */
  59. public HyphenationTree() {
  60. stoplist = new HashMap(23); // usually a small table
  61. classmap = new TernaryTree();
  62. vspace = new ByteVector();
  63. vspace.alloc(1); // this reserves index 0, which we don't use
  64. }
  65. /**
  66. * Packs the values by storing them in 4 bits, two values into a byte
  67. * Values range is from 0 to 9. We use zero as terminator,
  68. * so we'll add 1 to the value.
  69. * @param values a string of digits from '0' to '9' representing the
  70. * interletter values.
  71. * @return the index into the vspace array where the packed values
  72. * are stored.
  73. */
  74. protected int packValues(String values) {
  75. int i;
  76. int n = values.length();
  77. int m = (n & 1) == 1 ? (n >> 1) + 2 : (n >> 1) + 1;
  78. int offset = vspace.alloc(m);
  79. byte[] va = vspace.getArray();
  80. for (i = 0; i < n; i++) {
  81. int j = i >> 1;
  82. byte v = (byte)((values.charAt(i) - '0' + 1) & 0x0f);
  83. if ((i & 1) == 1) {
  84. va[j + offset] = (byte)(va[j + offset] | v);
  85. } else {
  86. va[j + offset] = (byte)(v << 4); // big endian
  87. }
  88. }
  89. va[m - 1 + offset] = 0; // terminator
  90. return offset;
  91. }
  92. /**
  93. * Unpack values.
  94. * @param k an integer
  95. * @return a string
  96. */
  97. protected String unpackValues(int k) {
  98. StringBuffer buf = new StringBuffer();
  99. byte v = vspace.get(k++);
  100. while (v != 0) {
  101. char c = (char)((v >>> 4) - 1 + '0');
  102. buf.append(c);
  103. c = (char)(v & 0x0f);
  104. if (c == 0) {
  105. break;
  106. }
  107. c = (char)(c - 1 + '0');
  108. buf.append(c);
  109. v = vspace.get(k++);
  110. }
  111. return buf.toString();
  112. }
  113. /**
  114. * Read hyphenation patterns from an XML file.
  115. * @param filename the filename
  116. * @throws HyphenationException In case the parsing fails
  117. */
  118. public void loadPatterns(String filename) throws HyphenationException {
  119. File f = new File(filename);
  120. try {
  121. InputSource src = new InputSource(f.toURI().toURL().toExternalForm());
  122. loadPatterns(src);
  123. } catch (MalformedURLException e) {
  124. throw new HyphenationException("Error converting the File '" + f + "' to a URL: "
  125. + e.getMessage());
  126. }
  127. }
  128. /**
  129. * Read hyphenation patterns from an XML file.
  130. * @param source the InputSource for the file
  131. * @throws HyphenationException In case the parsing fails
  132. */
  133. public void loadPatterns(InputSource source) throws HyphenationException {
  134. PatternParser pp = new PatternParser(this);
  135. ivalues = new TernaryTree();
  136. pp.parse(source);
  137. // patterns/values should be now in the tree
  138. // let's optimize a bit
  139. trimToSize();
  140. vspace.trimToSize();
  141. classmap.trimToSize();
  142. // get rid of the auxiliary map
  143. ivalues = null;
  144. }
  145. /**
  146. * Find pattern.
  147. * @param pat a pattern
  148. * @return a string
  149. */
  150. public String findPattern(String pat) {
  151. int k = super.find(pat);
  152. if (k >= 0) {
  153. return unpackValues(k);
  154. }
  155. return "";
  156. }
  157. /**
  158. * String compare, returns 0 if equal or
  159. * t is a substring of s.
  160. * @param s first character array
  161. * @param si starting index into first array
  162. * @param t second character array
  163. * @param ti starting index into second array
  164. * @return an integer
  165. */
  166. protected int hstrcmp(char[] s, int si, char[] t, int ti) {
  167. for (; s[si] == t[ti]; si++, ti++) {
  168. if (s[si] == 0) {
  169. return 0;
  170. }
  171. }
  172. if (t[ti] == 0) {
  173. return 0;
  174. }
  175. return s[si] - t[ti];
  176. }
  177. /**
  178. * Get values.
  179. * @param k an integer
  180. * @return a byte array
  181. */
  182. protected byte[] getValues(int k) {
  183. StringBuffer buf = new StringBuffer();
  184. byte v = vspace.get(k++);
  185. while (v != 0) {
  186. char c = (char)((v >>> 4) - 1);
  187. buf.append(c);
  188. c = (char)(v & 0x0f);
  189. if (c == 0) {
  190. break;
  191. }
  192. c = (char)(c - 1);
  193. buf.append(c);
  194. v = vspace.get(k++);
  195. }
  196. byte[] res = new byte[buf.length()];
  197. for (int i = 0; i < res.length; i++) {
  198. res[i] = (byte)buf.charAt(i);
  199. }
  200. return res;
  201. }
  202. /**
  203. * <p>Search for all possible partial matches of word starting
  204. * at index an update interletter values. In other words, it
  205. * does something like:</p>
  206. * <code>
  207. * for(i=0; i<patterns.length; i++) {
  208. * if ( word.substring(index).startsWidth(patterns[i]) )
  209. * update_interletter_values(patterns[i]);
  210. * }
  211. * </code>
  212. * <p>But it is done in an efficient way since the patterns are
  213. * stored in a ternary tree. In fact, this is the whole purpose
  214. * of having the tree: doing this search without having to test
  215. * every single pattern. The number of patterns for languages
  216. * such as English range from 4000 to 10000. Thus, doing thousands
  217. * of string comparisons for each word to hyphenate would be
  218. * really slow without the tree. The tradeoff is memory, but
  219. * using a ternary tree instead of a trie, almost halves the
  220. * the memory used by Lout or TeX. It's also faster than using
  221. * a hash table</p>
  222. * @param word null terminated word to match
  223. * @param index start index from word
  224. * @param il interletter values array to update
  225. */
  226. protected void searchPatterns(char[] word, int index, byte[] il) {
  227. byte[] values;
  228. int i = index;
  229. char p;
  230. char q;
  231. char sp = word[i];
  232. p = root;
  233. while (p > 0 && p < sc.length) {
  234. if (sc[p] == 0xFFFF) {
  235. if (hstrcmp(word, i, kv.getArray(), lo[p]) == 0) {
  236. values = getValues(eq[p]); // data pointer is in eq[]
  237. int j = index;
  238. for (int k = 0; k < values.length; k++) {
  239. if (j < il.length && values[k] > il[j]) {
  240. il[j] = values[k];
  241. }
  242. j++;
  243. }
  244. }
  245. return;
  246. }
  247. int d = sp - sc[p];
  248. if (d == 0) {
  249. if (sp == 0) {
  250. break;
  251. }
  252. sp = word[++i];
  253. p = eq[p];
  254. q = p;
  255. // look for a pattern ending at this position by searching for
  256. // the null char ( splitchar == 0 )
  257. while (q > 0 && q < sc.length) {
  258. if (sc[q] == 0xFFFF) { // stop at compressed branch
  259. break;
  260. }
  261. if (sc[q] == 0) {
  262. values = getValues(eq[q]);
  263. int j = index;
  264. for (int k = 0; k < values.length; k++) {
  265. if (j < il.length && values[k] > il[j]) {
  266. il[j] = values[k];
  267. }
  268. j++;
  269. }
  270. break;
  271. } else {
  272. q = lo[q];
  273. /**
  274. * actually the code should be:
  275. * q = sc[q] < 0 ? hi[q] : lo[q];
  276. * but java chars are unsigned
  277. */
  278. }
  279. }
  280. } else {
  281. p = d < 0 ? lo[p] : hi[p];
  282. }
  283. }
  284. }
  285. /**
  286. * Hyphenate word and return a Hyphenation object.
  287. * @param word the word to be hyphenated
  288. * @param remainCharCount Minimum number of characters allowed
  289. * before the hyphenation point.
  290. * @param pushCharCount Minimum number of characters allowed after
  291. * the hyphenation point.
  292. * @return a {@link Hyphenation Hyphenation} object representing
  293. * the hyphenated word or null if word is not hyphenated.
  294. */
  295. public Hyphenation hyphenate(String word, int remainCharCount,
  296. int pushCharCount) {
  297. char[] w = word.toCharArray();
  298. return hyphenate(w, 0, w.length, remainCharCount, pushCharCount);
  299. }
  300. /**
  301. * w = "****nnllllllnnn*****",
  302. * where n is a non-letter, l is a letter,
  303. * all n may be absent, the first n is at offset,
  304. * the first l is at offset + iIgnoreAtBeginning;
  305. * word = ".llllll.'\0'***",
  306. * where all l in w are copied into word.
  307. * In the first part of the routine len = w.length,
  308. * in the second part of the routine len = word.length.
  309. * Three indices are used:
  310. * index(w), the index in w,
  311. * index(word), the index in word,
  312. * letterindex(word), the index in the letter part of word.
  313. * The following relations exist:
  314. * index(w) = offset + i - 1
  315. * index(word) = i - iIgnoreAtBeginning
  316. * letterindex(word) = index(word) - 1
  317. * (see first loop).
  318. * It follows that:
  319. * index(w) - index(word) = offset - 1 + iIgnoreAtBeginning
  320. * index(w) = letterindex(word) + offset + iIgnoreAtBeginning
  321. */
  322. /**
  323. * Hyphenate word and return an array of hyphenation points.
  324. * @param w char array that contains the word
  325. * @param offset Offset to first character in word
  326. * @param len Length of word
  327. * @param remainCharCount Minimum number of characters allowed
  328. * before the hyphenation point.
  329. * @param pushCharCount Minimum number of characters allowed after
  330. * the hyphenation point.
  331. * @return a {@link Hyphenation Hyphenation} object representing
  332. * the hyphenated word or null if word is not hyphenated.
  333. */
  334. public Hyphenation hyphenate(char[] w, int offset, int len,
  335. int remainCharCount, int pushCharCount) {
  336. int i;
  337. char[] word = new char[len + 3];
  338. // normalize word
  339. char[] c = new char[2];
  340. int iIgnoreAtBeginning = 0;
  341. int iLength = len;
  342. boolean bEndOfLetters = false;
  343. for (i = 1; i <= len; i++) {
  344. c[0] = w[offset + i - 1];
  345. int nc = classmap.find(c, 0);
  346. if (nc < 0) { // found a non-letter character ...
  347. if (i == (1 + iIgnoreAtBeginning)) {
  348. // ... before any letter character
  349. iIgnoreAtBeginning++;
  350. } else {
  351. // ... after a letter character
  352. bEndOfLetters = true;
  353. }
  354. iLength--;
  355. } else {
  356. if (!bEndOfLetters) {
  357. word[i - iIgnoreAtBeginning] = (char)nc;
  358. } else {
  359. return null;
  360. }
  361. }
  362. }
  363. len = iLength;
  364. if (len < (remainCharCount + pushCharCount)) {
  365. // word is too short to be hyphenated
  366. return null;
  367. }
  368. int[] result = new int[len + 1];
  369. int k = 0;
  370. // check exception list first
  371. String sw = new String(word, 1, len);
  372. if (stoplist.containsKey(sw)) {
  373. // assume only simple hyphens (Hyphen.pre="-", Hyphen.post = Hyphen.no = null)
  374. ArrayList hw = (ArrayList)stoplist.get(sw);
  375. int j = 0;
  376. for (i = 0; i < hw.size(); i++) {
  377. Object o = hw.get(i);
  378. // j = index(sw) = letterindex(word)?
  379. // result[k] = corresponding index(w)
  380. if (o instanceof String) {
  381. j += ((String)o).length();
  382. if (j >= remainCharCount && j < (len - pushCharCount)) {
  383. result[k++] = j + iIgnoreAtBeginning;
  384. }
  385. }
  386. }
  387. } else {
  388. // use algorithm to get hyphenation points
  389. word[0] = '.'; // word start marker
  390. word[len + 1] = '.'; // word end marker
  391. word[len + 2] = 0; // null terminated
  392. byte[] il = new byte[len + 3]; // initialized to zero
  393. for (i = 0; i < len + 1; i++) {
  394. searchPatterns(word, i, il);
  395. }
  396. // hyphenation points are located where interletter value is odd
  397. // i is letterindex(word),
  398. // i + 1 is index(word),
  399. // result[k] = corresponding index(w)
  400. for (i = 0; i < len; i++) {
  401. if (((il[i + 1] & 1) == 1) && i >= remainCharCount
  402. && i <= (len - pushCharCount)) {
  403. result[k++] = i + iIgnoreAtBeginning;
  404. }
  405. }
  406. }
  407. if (k > 0) {
  408. // trim result array
  409. int[] res = new int[k];
  410. System.arraycopy(result, 0, res, 0, k);
  411. return new Hyphenation(new String(w, offset, len), res);
  412. } else {
  413. return null;
  414. }
  415. }
  416. /**
  417. * Add a character class to the tree. It is used by
  418. * {@link PatternParser PatternParser} as callback to
  419. * add character classes. Character classes define the
  420. * valid word characters for hyphenation. If a word contains
  421. * a character not defined in any of the classes, it is not hyphenated.
  422. * It also defines a way to normalize the characters in order
  423. * to compare them with the stored patterns. Usually pattern
  424. * files use only lower case characters, in this case a class
  425. * for letter 'a', for example, should be defined as "aA", the first
  426. * character being the normalization char.
  427. * @param chargroup a character class (group)
  428. */
  429. public void addClass(String chargroup) {
  430. if (chargroup.length() > 0) {
  431. char equivChar = chargroup.charAt(0);
  432. char[] key = new char[2];
  433. key[1] = 0;
  434. for (int i = 0; i < chargroup.length(); i++) {
  435. key[0] = chargroup.charAt(i);
  436. classmap.insert(key, 0, equivChar);
  437. }
  438. }
  439. }
  440. /**
  441. * Add an exception to the tree. It is used by
  442. * {@link PatternParser PatternParser} class as callback to
  443. * store the hyphenation exceptions.
  444. * @param word normalized word
  445. * @param hyphenatedword a vector of alternating strings and
  446. * {@link Hyphen hyphen} objects.
  447. */
  448. public void addException(String word, ArrayList hyphenatedword) {
  449. stoplist.put(word, hyphenatedword);
  450. }
  451. /**
  452. * Add a pattern to the tree. Mainly, to be used by
  453. * {@link PatternParser PatternParser} class as callback to
  454. * add a pattern to the tree.
  455. * @param pattern the hyphenation pattern
  456. * @param ivalue interletter weight values indicating the
  457. * desirability and priority of hyphenating at a given point
  458. * within the pattern. It should contain only digit characters.
  459. * (i.e. '0' to '9').
  460. */
  461. public void addPattern(String pattern, String ivalue) {
  462. int k = ivalues.find(ivalue);
  463. if (k <= 0) {
  464. k = packValues(ivalue);
  465. ivalues.insert(ivalue, (char)k);
  466. }
  467. insert(pattern, (char)k);
  468. }
  469. /**
  470. * Print statistics.
  471. */
  472. public void printStats() {
  473. System.out.println("Value space size = "
  474. + Integer.toString(vspace.length()));
  475. super.printStats();
  476. }
  477. /**
  478. * Main entry point for this hyphenation utility application.
  479. * @param argv array of command linee arguments
  480. * @throws Exception in case an exception is raised but not caught
  481. */
  482. public static void main(String[] argv) throws Exception {
  483. HyphenationTree ht = null;
  484. int minCharCount = 2;
  485. BufferedReader in
  486. = new BufferedReader(new java.io.InputStreamReader(System.in));
  487. while (true) {
  488. System.out.print("l:\tload patterns from XML\n"
  489. + "L:\tload patterns from serialized object\n"
  490. + "s:\tset minimum character count\n"
  491. + "w:\twrite hyphenation tree to object file\n"
  492. + "h:\thyphenate\n"
  493. + "f:\tfind pattern\n"
  494. + "b:\tbenchmark\n"
  495. + "q:\tquit\n\n"
  496. + "Command:");
  497. String token = in.readLine().trim();
  498. if (token.equals("f")) {
  499. System.out.print("Pattern: ");
  500. token = in.readLine().trim();
  501. System.out.println("Values: " + ht.findPattern(token));
  502. } else if (token.equals("s")) {
  503. System.out.print("Minimun value: ");
  504. token = in.readLine().trim();
  505. minCharCount = Integer.parseInt(token);
  506. } else if (token.equals("l")) {
  507. ht = new HyphenationTree();
  508. System.out.print("XML file name: ");
  509. token = in.readLine().trim();
  510. ht.loadPatterns(token);
  511. } else if (token.equals("L")) {
  512. ObjectInputStream ois = null;
  513. System.out.print("Object file name: ");
  514. token = in.readLine().trim();
  515. try {
  516. ois = new ObjectInputStream(new FileInputStream(token));
  517. ht = (HyphenationTree)ois.readObject();
  518. } catch (Exception e) {
  519. e.printStackTrace();
  520. } finally {
  521. if (ois != null) {
  522. try {
  523. ois.close();
  524. } catch (IOException e) {
  525. //ignore
  526. }
  527. }
  528. }
  529. } else if (token.equals("w")) {
  530. System.out.print("Object file name: ");
  531. token = in.readLine().trim();
  532. ObjectOutputStream oos = null;
  533. try {
  534. oos = new ObjectOutputStream(new FileOutputStream(token));
  535. oos.writeObject(ht);
  536. } catch (Exception e) {
  537. e.printStackTrace();
  538. } finally {
  539. if (oos != null) {
  540. try {
  541. oos.flush();
  542. } catch (IOException e) {
  543. //ignore
  544. }
  545. try {
  546. oos.close();
  547. } catch (IOException e) {
  548. //ignore
  549. }
  550. }
  551. }
  552. } else if (token.equals("h")) {
  553. System.out.print("Word: ");
  554. token = in.readLine().trim();
  555. System.out.print("Hyphenation points: ");
  556. System.out.println(ht.hyphenate(token, minCharCount,
  557. minCharCount));
  558. } else if (token.equals("b")) {
  559. if (ht == null) {
  560. System.out.println("No patterns have been loaded.");
  561. break;
  562. }
  563. System.out.print("Word list filename: ");
  564. token = in.readLine().trim();
  565. long starttime = 0;
  566. int counter = 0;
  567. try {
  568. BufferedReader reader
  569. = new BufferedReader(new FileReader(token));
  570. String line;
  571. starttime = System.currentTimeMillis();
  572. while ((line = reader.readLine()) != null) {
  573. // System.out.print("\nline: ");
  574. Hyphenation hyp = ht.hyphenate(line, minCharCount,
  575. minCharCount);
  576. if (hyp != null) {
  577. String hword = hyp.toString();
  578. // System.out.println(line);
  579. // System.out.println(hword);
  580. } else {
  581. // System.out.println("No hyphenation");
  582. }
  583. counter++;
  584. }
  585. } catch (Exception ioe) {
  586. System.out.println("Exception " + ioe);
  587. ioe.printStackTrace();
  588. }
  589. long endtime = System.currentTimeMillis();
  590. long result = endtime - starttime;
  591. System.out.println(counter + " words in " + result
  592. + " Milliseconds hyphenated");
  593. } else if (token.equals("q")) {
  594. break;
  595. }
  596. }
  597. }
  598. }