Subversion Repositories mkgmap

Rev

Rev 4377 | Blame | Compare with Previous | Last modification | View Log | RSS feed

/*
 * Copyright (C) 2010, 2011.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3 or
 * version 2 as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 */


package uk.me.parabola.mkgmap.srt;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import uk.me.parabola.imgfmt.ExitException;
import uk.me.parabola.imgfmt.app.srt.CodePosition;
import uk.me.parabola.imgfmt.app.srt.SRTFile;
import uk.me.parabola.imgfmt.app.srt.Sort;
import uk.me.parabola.imgfmt.fs.ImgChannel;
import uk.me.parabola.imgfmt.sys.FileImgChannel;
import uk.me.parabola.mkgmap.scan.SyntaxException;
import uk.me.parabola.mkgmap.scan.TokType;
import uk.me.parabola.mkgmap.scan.Token;
import uk.me.parabola.mkgmap.scan.TokenScanner;

/**
 * Read in a sort file from a text format.
 *
 * The file is in utf-8, regardless of the target codepage.
 *
 * The file should start with a codepage declaration, which determines the
 * target codepage for the sort.  This can be followed by a description which is
 * added into the SRT file.
 *
 * The characters are listed in order arranged in a way that shows the strength of the
 * difference between the characters. These are:
 *
 * Primary difference - different letters (eg a and b)
 * Secondary difference - different accents (eg a and a-acute)
 * Tertiary difference - different case (eg a and A)
 *
 * The sort order section begins with the word 'code'.
 *
 * Primary differences are represented by the less-than separator.
 * Secondary differences are represented by the semi-colon separator.
 * Tertiary differences are represented by the comma separator.
 *
 * Characters are represented in <emphasis>unicode (utf-8)</emphasis> (the whole file must be in utf-8).
 * Or alternatively you can use a four hex-digit number. A few special punctuation characters must
 * be written that way to prevent them being mistaken for separators.
 *
 * Example
 * <pre>
 * # This is a comment
 * codepage 1252
 * description "Example sort"
 * code a, A; â Â
 * < b, B
 * # Last two lines could be written:
 * # code a, A; â, Â < b, B
 * </pre>
 *
 * @author Steve Ratcliffe
 */

public class SrtTextReader {

        // States
        private static final int IN_INITIAL = 0;
        private static final int IN_CHARACTER = 1;
        private static final int IN_EXPAND = 2;
        private static final boolean EXPERIMENTAL = false;

        // Data that is read in, the output of the reading operation
        private final Sort sort = new Sort();

        private CharsetEncoder encoder;

        // Used during parsing.
        private int pos1;
        private int pos2;
        private int pos3;
        private int state;
        private String cflags = "";
        private Map<Integer, Integer> maxSec;
        private Map<Integer, Integer> maxTert;
        private List<CodePosition> expansions;

        public SrtTextReader(Reader r) {
                this("stream", r);
        }

        private SrtTextReader(String filename) throws IOException {
                this(filename, new InputStreamReader(new FileInputStream(filename), StandardCharsets.UTF_8));
        }

        private SrtTextReader(String filename, Reader r) {
                maxSec = new HashMap<>();
                maxTert = new HashMap<>();
                expansions = new ArrayList<>();
                read(filename, r);
                maxSec = null;
                maxTert = null;
                expansions = null;
        }

        /**
         * Find and read in the default sort description for the given codepage.
         */

        public static Sort sortForCodepage(int codepage) {
                String name = "sort/cp" + codepage + ".txt";
                try (InputStream is = Sort.class.getClassLoader().getResourceAsStream(name)) {
                        if (is == null) {
                                if (codepage == 1252)
                                        throw new ExitException("No sort description for code-page 1252 available");

                                Sort defaultSort = SrtTextReader.sortForCodepage(1252);
                                defaultSort.setCodepage(codepage);
                                defaultSort.setDescription("Default sort");
                                return defaultSort;
                        }

                        InputStreamReader r = new InputStreamReader(is, StandardCharsets.UTF_8);
                        SrtTextReader sr = new SrtTextReader(r);
                        return sr.getSort();
                } catch (IOException e) {
                        throw new ExitException("Exception " + e + " getting sortForCodepage " + codepage);
                }
        }

        /**
         * Read in a file and save the information in a form that can be used
         * to compare strings.
         * @param filename The name of the file, used for display purposes. It need
         * not refer to a file that actually exists.
         * @param r The opened file or other readable source.
         * @throws SyntaxException If the format of the file is incorrect.
         */

        public void read(String filename, Reader r) {
                TokenScanner scanner = new TokenScanner(filename, r);
                resetPos();
                state = IN_INITIAL;
                while (!scanner.isEndOfFile()) {
                        Token tok = scanner.nextToken();

                        // We deal with whole line comments here
                        if (tok.isValue("#")) {
                                scanner.skipLine();
                                continue;
                        }

                        switch (state) {
                        case IN_INITIAL:
                                initialState(scanner, tok);
                                break;
                        case IN_CHARACTER:
                                characterState(scanner, tok);
                                break;
                        case IN_EXPAND:
                                expandState(scanner, tok);
                                break;
                        }
                }
                sort.setExpansions(expansions);
                sort.finish();
        }

        /**
         * The initial state, looking for a variable to set or a command to change
         * the state.
         * @param scanner The scanner for more tokens.
         * @param tok The first token to process.
         */

        private void initialState(TokenScanner scanner, Token tok) {
                String val = tok.getValue();
                TokType type = tok.getType();
                if (type == TokType.TEXT) {
                        switch (val) {
                        case "codepage":
                                int codepage = scanner.nextInt();
                                sort.setCodepage(codepage);
                                encoder = sort.getCharset().newEncoder();
                                break;
                        case "description":
                                sort.setDescription(scanner.nextWord());
                                break;
                        case "id1":
                                sort.setId1(scanner.nextInt());
                                break;
                        case "id2":
                                sort.setId2(scanner.nextInt());
                                break;
                        case "multi":
                                sort.setMulti(true);
                                break;

                        case "code":  // The old name; use characters
                        case "characters":
                                if (encoder == null)
                                        throw new SyntaxException(scanner, "Missing codepage declaration before code");
                                state = IN_CHARACTER;
                                scanner.skipSpace();
                                break;
                        case "expand":
                                state = IN_EXPAND;
                                scanner.skipSpace();
                                break;
                        default:
                                throw new SyntaxException(scanner, "Unrecognised command " + val);
                        }
                }
        }

        /**
         * Block consisting of characters and relations between them.
         *
         * The sort order is derived from this.
         *
         * @param scanner The scanner for more tokens.
         * @param tok The current token to process.
         */

        private void characterState(TokenScanner scanner, Token tok) {
                String val = tok.getValue();
                TokType type = tok.getType();
                if (type == TokType.TEXT) {
                        switch (val) {
                        case "flags":
                                scanner.validateNext("=");
                                cflags = scanner.nextWord();
                                // TODO not yet
                                break;
                        case "pos": // Used to set the actual sort position value, not used any more
                                scanner.validateNext("=");
                                try {
                                        int newPos = Integer.decode(scanner.nextWord());
                                        if (newPos < pos1)
                                                throw new SyntaxException(scanner, "cannot set primary position backwards, was " + pos1);
                                        pos1 = newPos;
                                } catch (NumberFormatException e) {
                                        throw new SyntaxException(scanner, "invalid integer for position");
                                }
                                break;
                        case "pos2": // Used to set the actual sort position value, not used any more
                                scanner.validateNext("=");
                                pos2 = Integer.decode(scanner.nextWord());
                                break;
                        case "pos3": // Used to set the actual sort position value, not used any more
                                scanner.validateNext("=");
                                pos3 = Integer.decode(scanner.nextWord());
                                break;
                        case "code":  // the old name, use 'characters'
                        case "characters":
                                advancePos();
                                break;
                        case "expand":
                                //scanner.pushToken(tok);
                                state = IN_EXPAND;
                                break;
                        default:
                                addCharacter(scanner, val);
                                break;
                        }
                } else if (type == TokType.SYMBOL) {
                        switch (val) {
                        case "=":
                                break;
                        case ",":
                                pos3++;
                                break;
                        case ";":
                                pos3 = 1;
                                pos2++;
                                break;
                        case "<":
                                advancePos();
                                break;
                        default:
                                addCharacter(scanner, val);
                                break;
                        }

                }
        }

        /**
         * Within an 'expand' command. The whole command is read before return, they can not span
         * lines.
         *
         * @param tok The first token after the keyword.
         */

        private void expandState(TokenScanner scanner, Token tok) {
                String val = tok.getValue();

                Code code = new Code(scanner, val).read();

                String s = scanner.nextValue();
                if (!"to".equals(s))
                        throw new SyntaxException(scanner, "Expected the word 'to' in expand command");

                int secondary = 0;
                int tertiary = 0;
                int num = 0;
                while (!scanner.isEndOfFile()) {
                        Token t = scanner.nextRawToken();
                        if (t.isEol())
                                break;
                        if (t.isWhiteSpace())
                                continue;

                        Code r = new Code(scanner, t.getValue()).read();

                        CodePosition cp = new CodePosition();
                        int b = r.getBval();
                        int primary = sort.getPrimary(b);
                        cp.setPrimary(primary);

                        // We do not want the character to sort fully equal to the expanded characters (or any other
                        // character so adjust the ordering at other strengths.  May need further tweaks.
                        if (EXPERIMENTAL) {
                                secondary = sort.getSecondary(b);
                                tertiary = sort.getTertiary(b);
                                if (num++ == 0) {
                                        Integer max = maxSec.get(primary);
                                        secondary += max == null ? 0 : max;
                                        if (charFlags(code) == 1) {
                                                max = maxTert.get(primary);
                                                tertiary += max == null ? 0 : max;
                                        }
                                } else {
                                        secondary = 1;
                                }
                                cp.setSecondary(secondary);
                                cp.setTertiary(tertiary);
                        } else {
                                num++;
                                secondary = sort.getSecondary(b);
                                cp.setSecondary(secondary + 7);

                                tertiary = sort.getTertiary(b);
                                cp.setTertiary(tertiary + 2);
                        }
                        expansions.add(cp);
                }

                int flags = charFlags(code) | (num-1) << 4;
                sort.add(code.getBval(), expansions.size() - num + 1, 0, 0, flags);
                state = IN_INITIAL;
        }

        /**
         * Add a character to the sort table.
         * @param scanner Input scanner, for line number information.
         * @param val A single character string containing the character to be added. This will
         * be either a single character which is the unicode representation of the character, or
         * two or more characters which is the hex representation of the code point in the target codepage.
         */

        private void addCharacter(TokenScanner scanner, String val) {
                Code code = new Code(scanner, val).read();
                setSortcode(code);
        }

        /**
         * Set the sort code for the given 8-bit character.
         * @param c the code instance
         */

        private void setSortcode(Code c) {
                int flags = charFlags(c);
                if (cflags.contains("0"))
                        flags = 0;
                sort.add(c.getBval(), pos1, pos2, pos3, flags);
                this.cflags = "";

                if (EXPERIMENTAL) {
                        Integer max = maxSec.get(pos1);
                        if (max == null)
                                max = pos2;
                        else
                                max = Math.max(pos2, max);
                        maxSec.put(pos1, max);
                        max = maxTert.get(pos1);
                        if (max == null)
                                max = pos3;
                        else
                                max = Math.max(pos3, max);
                        maxTert.put(pos3, max);
                }
        }

        /**
         * The flags that describe the kind of character. Known ones
         * are letter and digit. There may be others.
         * @param c The actual code instance
         * @return The flags that apply to it.
         */

        private int charFlags(Code c) {
                int flags = 0;
                if (c.getBval() <= 0xff) {
                        int ch = c.getCval();
                        if (ch == 'ª' || (Character.isLetter(ch) && (Character.getType(ch) & Character.MODIFIER_LETTER) == 0))
                                flags = 1;
                        if (Character.isDigit(ch))
                                flags = 2;
                }
                return flags;
        }

        /**
         * Reset the position fields to their initial values.
         */

        private void resetPos() {
                pos1 = 0;
                pos2 = 0;
                pos3 = 0;
        }

        /**
         * Advance the major position value, resetting the minor position variables.
         */

        private void advancePos() {
                if (pos1 == 0)
                        pos1 = 1;
                else
                        pos1 += pos2;
                pos2 = 1;
                pos3 = 1;
        }

        public Sort getSort() {
                return sort;
        }

        /**
         * Read in a sort description text file and create a SRT from it.
         * @param args First arg is the text input file, the second is the name of the output file. The defaults are
         * in.txt and out.srt.
         */

        public static void main(String[] args) throws IOException {
                String infile = "in.txt";
                if (args.length > 0)
                        infile = args[0];

                String outfile = "out.srt";
                if (args.length > 1)
                        outfile = args[1];
                try {
                        Files.delete(Paths.get(outfile, ""));
                } catch (Exception e) {
                }
                ImgChannel chan = new FileImgChannel(outfile, "rw");
                SRTFile sf = new SRTFile(chan);

                SrtTextReader tr = new SrtTextReader(infile);
                Sort sort1 = tr.getSort();
                sf.setSort(sort1);
                sf.write();
                sf.close();
                chan.close();
        }

        /**
         * Helper to represent a code read from the file.
         *
         * You can write it in unicode, or as a hex number.
         * We work out what you wrote, and return both the code point in
         * the codepage and the unicode character form of the letter.
         */

        private class Code {
                private final TokenScanner scanner;
                private final String val;
                private int cval;
                private int bval;

                public Code(TokenScanner scanner, String val) {
                        this.scanner = scanner;
                        this.val = val;
                }

                /**
                 * Get the character encoded in the code-page encoding.
                 *
                 * It will be one byte for the format-9 code pages cp1252 etc.
                 * @return A character encoded in the code-page.
                 */

                public int getBval() {
                        return bval;
                }

                /**
                 * Get the character in unicode.
                 *
                 * It will in general be a 2 byte value.
                 *
                 * @return The character expressed in unicode.
                 */

                public int getCval() {
                        return cval;
                }

                public Code read() {
                        try {
                                if (val.length() == 1) {
                                        cval = val.charAt(0);
                                } else {
                                        cval = Integer.parseInt(val, 16);
                                }

                                if (sort.isMulti()) {
                                        bval = cval;
                                } else {
                                        CharBuffer cbuf = CharBuffer.wrap(new char[] {(char) cval});
                                        ByteBuffer out = encoder.encode(cbuf);
                                        if (out.remaining() > 1)
                                                throw new SyntaxException(scanner, "more than one character resulted from conversion of " + val);

                                        bval = out.get() & 0xff;
                                }

                        } catch (NumberFormatException e) {
                                throw new SyntaxException(scanner, "Not a valid hex number " + val);
                        } catch (CharacterCodingException e) {
                                throw new SyntaxException(scanner, "Character not valid in character set '" + val + "'");
                        }
                        return this;
                }

                public String toString() {
                        return String.format("%x", cval);
                }
        }
}