package info.toyonos.subtitles4j.factory;
import info.toyonos.subtitles4j.model.SubtitlesContainer;
import info.toyonos.subtitles4j.model.SubtitlesContainer.Caption;
import info.toyonos.subtitles4j.model.SubtitlesContainer.StyleProperty;
import java.io.IOException;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;
import org.apache.commons.lang3.StringUtils;
import org.ini4j.Config;
import org.ini4j.Ini;
import org.ini4j.Profile.Section;
import com.google.common.collect.BiMap;
import com.google.common.collect.ImmutableBiMap;
// TODO handle SSA
public class ASSFactory extends AbstractFormatFactory
{
private static final SimpleDateFormat TIMESTAMPS_SDF = new SimpleDateFormat("H:mm:ss:SSS");
private static final String SCRIPT_INFO = "Script Info";
//private static final String V4_STYLE = "V4 Styles";
private static final String V4PLUS_STYLE = "V4+ Styles";
private static final String EVENTS = "Events";
private static final String FORMAT = "Format";
private static final String DIALOGUE = "Dialogue";
private static final String STYLE = "Style";
private static final String SCRIPT_INFO_TITLE = "Title";
private static final String SCRIPT_INFO_AUTHOR = "Original Script";
private static final String SCRIPT_INFO_TYPE = "ScriptType";
private static final String SCRIPT_INFO_TIMER = "Timer";
private static final String FORMAT_LAYER = "Layer";
//private static final String FORMAT_MARKED = "Marked";
private static final String FORMAT_START = "Start";
private static final String FORMAT_END = "End";
private static final String FORMAT_STYLE = "Style";
private static final String FORMAT_NAME = "Name";
private static final String FORMAT_MARGINL = "MarginL";
private static final String FORMAT_MARGINR = "MarginR";
private static final String FORMAT_MARGINV = "MarginV";
private static final String FORMAT_EFFECT = "Effect";
private static final String FORMAT_TEXT = "Text";
private static final String SEPARATOR = ": ";
private static final String VALUE_SEPARATOR = ",";
private static final String SCRIPT_TYPE = "v4.00+";
private static final String DEFAULT_STYLE = "Default";
private static final String DEFAULT_MARGIN = "0000";
private static final BiMap<StyleProperty, StyleMapping> STYLE_MAPPING;
static
{
STYLE_MAPPING = new ImmutableBiMap.Builder<StyleProperty, StyleMapping>()
.put(StyleProperty.NAME, new StyleMapping("Name"))
.put(StyleProperty.FONT_NAME, new StyleMapping("Fontname"))
.put(StyleProperty.FONT_SIZE, new StyleMapping("Fontsize"))
.put(StyleProperty.PRIMARY_COLOR, new StyleMapping("PrimaryColour"))
.put(StyleProperty.SECONDARY_COLOR, new StyleMapping("SecondaryColour", StyleProperty.PRIMARY_COLOR))
.put(StyleProperty.OUTLINE_COLOR, new StyleMapping("OutlineColour", StyleProperty.PRIMARY_COLOR))
.put(StyleProperty.BACK_COLOR, new StyleMapping("BackColour"))
.put(StyleProperty.BOLD, new StyleMapping("Bold", "0"))
.put(StyleProperty.ITALIC, new StyleMapping("Italic", "0"))
.put(StyleProperty.UNDERLINE, new StyleMapping("Underline", "0"))
.put(StyleProperty.STRIKEOUT, new StyleMapping("StrikeOut", "0"))
.put(StyleProperty.SCALE_X, new StyleMapping("ScaleX", "100"))
.put(StyleProperty.SCALE_Y, new StyleMapping("ScaleY", "100"))
.put(StyleProperty.SPACING, new StyleMapping("Spacing", "0"))
.put(StyleProperty.ANGLE, new StyleMapping("Angle", "0"))
.put(StyleProperty.BORDER_STYLE, new StyleMapping("BorderStyle", "1"))
.put(StyleProperty.OUTLINE, new StyleMapping("Outline", "2"))
.put(StyleProperty.SHADOW, new StyleMapping("Shadow", "2"))
.put(StyleProperty.ALIGNMENT, new StyleMapping("Alignment", "2"))
.put(StyleProperty.MARGIN_L, new StyleMapping("MarginL", "0"))
.put(StyleProperty.MARGIN_R, new StyleMapping("MarginR", "0"))
.put(StyleProperty.MARGIN_V, new StyleMapping("MarginV", "0"))
.put(StyleProperty.ENCODING, new StyleMapping("Encoding", "0"))
.build();
}
protected ASSFactory() {}
@Override
public SubtitlesContainer fromStream(InputStream input) throws MalformedSubtitlesException, IOException
{
SubtitlesContainer container = new SubtitlesContainer();
Ini iniFile = new Ini();
Config conf = new Config();
conf.setEscape(false);
iniFile.setConfig(conf);
iniFile.load(input);
// ### Script Info section : metadata ###
Section scriptInfoSection = getSection(iniFile, SCRIPT_INFO);
// Title
container.setTitle(scriptInfoSection.get(SCRIPT_INFO_TITLE));
// Author
container.setAuthor(scriptInfoSection.get(SCRIPT_INFO_AUTHOR));
// Type verification
if (!SCRIPT_TYPE.equals(scriptInfoSection.get(SCRIPT_INFO_TYPE)))
{
throw new MalformedSubtitlesException(String.format(
"[Script Info] : invalid value for %s, expected '%s', found '%s',",
SCRIPT_INFO_TYPE,
SCRIPT_TYPE,
scriptInfoSection.get(SCRIPT_INFO_TYPE)));
}
// Timer
float timer = Float.parseFloat(scriptInfoSection.get(SCRIPT_INFO_TIMER, "100").replace(',', '.'));
// ### V4+ Styles section ###
Section stylesSection = getSection(iniFile, V4PLUS_STYLE);
String[] styleFormat = stylesSection.get(FORMAT).split("\\s*,\\s*");
BiMap<StyleMapping, StyleProperty> mappings = STYLE_MAPPING.inverse();
// For each defined style
for (int i = 0; i < stylesSection.length(STYLE); i++)
{
String[] styleValues = stylesSection.get(STYLE, i).split(",");
Map<StyleProperty, String> styleValuesMap = null;
// For each value of this style
for (int j = 0; j < styleFormat.length; j++)
{
StyleProperty property = mappings.get(styleFormat[j]);
if (property != null)
{
if (property == StyleProperty.NAME)
{
// The style's key
styleValuesMap = new HashMap<SubtitlesContainer.StyleProperty, String>();
container.getStyles().put(styleValues[j], styleValuesMap);
}
else
{
// Regular property
if (styleValuesMap != null)
{
styleValuesMap.put(property, styleValues[j]);
}
else
{
// Key has not been initialized
logger.warn("The current style has no name defined, ignoring this property : {}", property);
break;
}
}
}
else
{
// Unknown property
logger.warn("The property {} is unknown, ignoring it", property);
}
}
}
// ### Event section : captions ###
Section eventsSection = getSection(iniFile, EVENTS);
List<String> eventFormat = Arrays.asList(eventsSection.get(FORMAT).split("\\s*,\\s*"));
// Start index
int idxStart = getIndex(eventFormat, FORMAT_START);
// End index
int idxEnd = getIndex(eventFormat, FORMAT_END);
// Style index
int idxStyle = getIndex(eventFormat, FORMAT_STYLE);
// Text
int idxText = getIndex(eventFormat, FORMAT_TEXT);
for (int i = 0; i < eventsSection.length(DIALOGUE); i++)
{
List<String> dialogue = Arrays.asList(eventsSection.get(DIALOGUE, i).split(","));
long start = (long) (getMilliseconds(dialogue.get(idxStart) + "0") * timer / 100);
long end = (long) (getMilliseconds(dialogue.get(idxEnd) + "0") * timer / 100);
String styleName = dialogue.get(idxStyle);
List<String> subtitlesLines = Arrays.asList(dialogue.get(idxText).replaceAll("\\{.*?\\}", "").split("\\\\n|\\\\N"));
// Checking the style
if (container.getStyles().get(styleName) == null)
{
throw new MalformedSubtitlesException("[Events] : the style '" + styleName + "' is not defined");
}
// Adding the caption
container.addCaption(start, end, styleName, subtitlesLines);
}
logger.trace("The subtitles source has been read with success, {} captions read", container.getCaptions().size());
return container;
}
private int getIndex(List<String> format, String key) throws MalformedSubtitlesException
{
int idx = format.indexOf(key);
if (idx == -1)
{
throw new MalformedSubtitlesException("[Events] : missing '" + key.toLowerCase() + "' key in format");
}
return idx;
}
private Section getSection(Ini iniFile, String sectionName) throws MalformedSubtitlesException
{
Section section = iniFile.get(sectionName);
if (section == null) throw new MalformedSubtitlesException("Missing section [" + sectionName + "]");
return section;
}
@Override
public void visit(SubtitlesContainer container) throws SubtitlesGenerationException
{
super.visit(container);
subtitlesWriter.println("[" + SCRIPT_INFO + "]");
subtitlesWriter.println(SCRIPT_INFO_TITLE + SEPARATOR + container.getTitle());
subtitlesWriter.println(SCRIPT_INFO_AUTHOR + SEPARATOR + container.getAuthor());
subtitlesWriter.println(SCRIPT_INFO_TYPE + SEPARATOR + SCRIPT_TYPE);
if (!container.getStyles().isEmpty())
{
subtitlesWriter.println("\n[" + V4PLUS_STYLE + "]");
subtitlesWriter.println(FORMAT + SEPARATOR + StringUtils.join(STYLE_MAPPING.values(), ", "));
for (Map.Entry<String, Map<StyleProperty, String>> styleMapEntry : container.getStyles().entrySet())
{
subtitlesWriter.print(STYLE + SEPARATOR + styleMapEntry.getKey() + VALUE_SEPARATOR);
Map<StyleProperty, String> styleValues = styleMapEntry.getValue();
TreeSet<StyleProperty> properties = new TreeSet<SubtitlesContainer.StyleProperty>(STYLE_MAPPING.keySet());
for (StyleProperty property : properties)
{
// Name is already handled
if (property == StyleProperty.NAME) continue;
// Printing the right value
printStyleValue(property, styleValues);
if (properties.last() != property) subtitlesWriter.print(VALUE_SEPARATOR);
}
subtitlesWriter.println();
}
}
subtitlesWriter.println("\n[" + EVENTS + "]");
subtitlesWriter.println(
FORMAT +
SEPARATOR +
StringUtils.join(new String[] {
FORMAT_LAYER,
FORMAT_START,
FORMAT_END,
FORMAT_STYLE,
FORMAT_NAME,
FORMAT_MARGINL,
FORMAT_MARGINR,
FORMAT_MARGINV,
FORMAT_EFFECT,
FORMAT_TEXT},
", ")
);
}
private void printStyleValue(StyleProperty property, Map<StyleProperty, String> styleValues) throws SubtitlesGenerationException
{
StyleMapping mapping = STYLE_MAPPING.get(property);
String value = styleValues.get(property);
if (value != null)
{
subtitlesWriter.print(value);
}
else if (!mapping.mandatory)
{
// Default value
if (mapping.defaultValue != null)
{
subtitlesWriter.print(mapping.defaultValue);
}
else if (mapping.mirroredProperty != null)
{
// If STYLE_MAPPING contains a StyleProperty mirrored on itself, infinite loop !
printStyleValue(mapping.mirroredProperty, styleValues);
}
else
{
throw new IllegalArgumentException("Invalid mapping '" + mapping.name + "', no default value or mirrored property");
}
}
else
{
// Mandatory field with no value
throw new SubtitlesGenerationException("The style property " + property + " does not have any value available");
}
}
@Override
public void visit(Caption caption) throws SubtitlesGenerationException
{
super.visit(caption);
subtitlesWriter.print(DIALOGUE + SEPARATOR);
subtitlesWriter.print(0); // Layer
subtitlesWriter.print(VALUE_SEPARATOR);
subtitlesWriter.print(StringUtils.chop(formatMilliseconds(caption.getStart()))); // Start
subtitlesWriter.print(VALUE_SEPARATOR);
subtitlesWriter.print(StringUtils.chop(formatMilliseconds(caption.getEnd()))); // End
subtitlesWriter.print(VALUE_SEPARATOR);
subtitlesWriter.print(caption.getStyleKey() != null ? caption.getStyleKey() : DEFAULT_STYLE); // Style
subtitlesWriter.print(VALUE_SEPARATOR);
subtitlesWriter.print(""); // Name
subtitlesWriter.print(VALUE_SEPARATOR);
subtitlesWriter.print(DEFAULT_MARGIN); // MarginL
subtitlesWriter.print(VALUE_SEPARATOR);
subtitlesWriter.print(DEFAULT_MARGIN); // MarginR
subtitlesWriter.print(VALUE_SEPARATOR);
subtitlesWriter.print(DEFAULT_MARGIN); // MarginV
subtitlesWriter.print(VALUE_SEPARATOR);
subtitlesWriter.print(""); // Effect
subtitlesWriter.print(VALUE_SEPARATOR);
subtitlesWriter.println(StringUtils.join(caption.getLines(), "\\N"));
}
@Override
protected SimpleDateFormat getTimestampDateFormat()
{
return TIMESTAMPS_SDF;
}
}