/* * (C) Copyright 2014 Boni Garcia (http://bonigarcia.github.io/) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * 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. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package io.github.bonigarcia.dualsub.srt; import io.github.bonigarcia.dualsub.util.I18N; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.channels.FileChannel; import java.nio.charset.Charset; import java.nio.charset.CharsetEncoder; import java.nio.charset.CodingErrorAction; import java.text.ParseException; import java.util.Date; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.TreeMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * DualSrt. * * @author Boni Garcia (boni.gg@gmail.com) * @since 1.0.0 */ public class DualSrt { private static final Logger log = LoggerFactory.getLogger(DualSrt.class); private TreeMap<String, Entry[]> subtitles; private int signatureGap; private int signatureTime; private int gap; private int desync; private int extension; private boolean extend; private boolean progressive; private final String SPACE_HTML = " "; public DualSrt(Properties properties, int desync, boolean extend, int extension, boolean progressive) { this.extend = extend; this.extension = extension; this.progressive = progressive; this.subtitles = new TreeMap<String, Entry[]>(); this.signatureGap = Integer.parseInt(properties .getProperty("signatureGap")); // seconds this.signatureTime = Integer.parseInt(properties .getProperty("signatureTime")); // seconds this.gap = Integer.parseInt(properties.getProperty("gap")); // milliseconds this.desync = desync; } public void addEntry(String time, Entry[] entries) { this.subtitles.put(time, entries); } public void addAll(Map<String, Entry> allSubs) { for (String t : allSubs.keySet()) { addEntry(t, new Entry[] { allSubs.get(t) }); } } public void log() { String left = null, right = null, line = ""; for (String time : subtitles.keySet()) { for (int i = 0; i < Math.max(subtitles.get(time)[0].size(), subtitles.get(time)[1].size()); i++) { left = i < subtitles.get(time)[0].size() ? subtitles.get(time)[0] .get(i) : SrtUtils.getBlankLine(); right = i < subtitles.get(time)[1].size() ? subtitles.get(time)[1] .get(i) : SrtUtils.getBlankLine(); line = left + right; log.info(time + " " + line + " " + SrtUtils.getWidth(line)); } log.info(""); } } private void addPadding() { Map<String, Entry[]> output = new TreeMap<String, Entry[]>(); Entry[] newEntry; for (String t : subtitles.keySet()) { newEntry = new Entry[2]; for (int i = 0; i < 2; i++) { if (isLong(subtitles.get(t)[i])) { newEntry[i] = this.splitEntry(subtitles.get(t)[i]); } else { newEntry[i] = this.noSplitEntry(subtitles.get(t)[i]); } output.put(t, newEntry); } } subtitles = new TreeMap<String, Entry[]>(output); } /** * It checks whether or not an entry (collection of entries) is long (width * upper than half_width) * * @param entry * @return */ private boolean isLong(Entry entry) { boolean split = false; float width; for (int i = 0; i < entry.size(); i++) { width = SrtUtils.getWidth(entry.get(i)); if (width > SrtUtils.getHalfWidth()) { split = true; break; } } return split; } /** * It converts and entry adding the padding and splitting lines. * * @param entry * @return */ private Entry splitEntry(Entry entry) { Entry newEntry = new Entry(); String append = ""; for (int i = 0; i < entry.size(); i++) { append += entry.get(i) + SrtUtils.getSpace(); } append = append.trim(); String[] words = append.split(SrtUtils.getSpace()); List<String> ensuredArray = ensureArray(words); String newLine = ""; for (int i = 0; i < ensuredArray.size(); i++) { if (SrtUtils.getWidth(newLine + ensuredArray.get(i)) < SrtUtils .getHalfWidth()) { newLine += ensuredArray.get(i) + SrtUtils.getSpace(); } else { newEntry.add(convertLine(newLine.trim())); newLine = ensuredArray.get(i) + SrtUtils.getSpace(); } } if (!newLine.isEmpty()) { newEntry.add(convertLine(newLine.trim())); } return newEntry; } /** * It converts and entry adding the padding but without splitting lines. * * @param entry * @return */ private Entry noSplitEntry(Entry entry) { Entry newEntry = new Entry(entry); for (int i = 0; i < entry.size(); i++) { newEntry.set(i, convertLine(entry.get(i))); } return newEntry; } /** * Ensures that each word in the arrays in no longer than MAXWIDTH * * @param words * @return */ private List<String> ensureArray(String[] words) { List<String> ensured = new LinkedList<String>(); for (String s : words) { if (SrtUtils.getWidth(s) <= SrtUtils.getHalfWidth()) { ensured.add(s); } else { int sLength = s.length(); ensured.add(s.substring(0, sLength / 2)); ensured.add(s.substring(sLength / 2, sLength / 2)); } } return ensured; } /** * It adds the padding (SEPARATOR + SPACEs) to a single line. * * @param line * @return */ private String convertLine(String line) { float width = SrtUtils.getWidth(line); double diff = ((SrtUtils.getHalfWidth() - width) / SrtUtils .getSpaceWidth()) / 2; double rest = diff % 1; int numSpaces = (int) Math.floor(diff); String additional = (rest >= 0.5) ? SrtUtils.getPadding() : ""; if (numSpaces < 0) { numSpaces = 0; } String newLine = SrtUtils.getSeparator() + SrtUtils.repeat(SrtUtils.getPadding(), numSpaces + 1) + additional + line + SrtUtils.repeat(SrtUtils.getPadding(), numSpaces + 1) + SrtUtils.getSeparator(); return newLine; } public void processDesync(String time, Entry desyncEntry) throws ParseException { TreeMap<String, Entry[]> newSubtitles = new TreeMap<String, Entry[]>(); Date inTime = SrtUtils.getInitTime(time); Date enTime = SrtUtils.getEndTime(time); long initTime = inTime != null ? inTime.getTime() : 0; long endTime = enTime != null ? enTime.getTime() : 0; long iTime, jTime; int top = 0, down = subtitles.keySet().size(); boolean topOver = false, downOver = false; int from, to; for (String t : subtitles.keySet()) { inTime = SrtUtils.getInitTime(time); enTime = SrtUtils.getEndTime(time); iTime = inTime != null ? SrtUtils.getInitTime(t).getTime() : 0; jTime = enTime != null ? SrtUtils.getEndTime(t).getTime() : 0; if (iTime <= initTime) { top++; } if (jTime >= endTime) { down--; } if (iTime <= initTime && jTime >= initTime) { topOver = true; } if (iTime <= endTime && jTime >= endTime) { downOver = true; } } from = top - 1 + (topOver ? 0 : 1); to = down - (downOver ? 0 : 1); log.debug(time + " TOP " + top + " DOWN " + down + " TOPOVER " + topOver + " DOWNOVER " + downOver + " SIZE " + subtitles.size() + " FROM " + from + " TO " + to + " " + desyncEntry.getSubtitleLines()); String mixedTime = mixTime(initTime, endTime, from, to); log.debug(mixedTime); Entry newEntryLeft = new Entry(); Entry newEntryRight = new Entry(); for (int i = from; i <= to; i++) { newEntryLeft .addAll(subtitles.get(subtitles.keySet().toArray()[i])[0]); newEntryRight .addAll(subtitles.get(subtitles.keySet().toArray()[i])[1]); } newEntryRight.addAll(desyncEntry); if (top != 0) { newSubtitles.putAll(subtitles.subMap(subtitles.firstKey(), true, (String) subtitles.keySet().toArray()[top - 1], !topOver)); } newSubtitles .put(mixedTime, new Entry[] { newEntryLeft, newEntryRight }); if (down != subtitles.size()) { newSubtitles.putAll(subtitles.subMap((String) subtitles.keySet() .toArray()[down], !downOver, subtitles.lastKey(), true)); } subtitles = newSubtitles; } private String mixTime(long initTime, long endTime, int from, int to) throws ParseException { long initCandidate = initTime; long endCandidate = endTime; long initFromTime = initTime; long endToTime = endTime; if (to > 0 && to >= from) { if (from < subtitles.size()) { Date iTime = SrtUtils.getInitTime((String) subtitles.keySet() .toArray()[from]); if (iTime != null) { initFromTime = iTime.getTime(); } } if (to < subtitles.size()) { Date eTime = SrtUtils.getEndTime((String) subtitles.keySet() .toArray()[to]); if (eTime != null) { endToTime = eTime.getTime(); } } switch (getDesync()) { case 0: // Left initCandidate = initFromTime; endCandidate = endToTime; break; case 1: // Right initCandidate = initTime; endCandidate = endTime; break; case 2: // Max initCandidate = Math.min(initTime, initFromTime); endCandidate = Math.max(endTime, endToTime); break; case 3: // Min initCandidate = Math.max(initTime, initFromTime); endCandidate = Math.min(endTime, endToTime); break; } } return SrtUtils.createSrtTime(new Date(initCandidate), new Date( endCandidate)); } /** * It writes a new subtitle file (SRT) from a subtitle map. * * @param subs * @param fileOuput * @throws IOException * @throws ParseException */ public void writeSrt(String fileOuput, String charsetStr, boolean translate, boolean merge) throws IOException, ParseException { boolean horizontal = SrtUtils.isHorizontal(); if (!horizontal && (!translate || (translate & merge))) { addPadding(); } if (extend) { shiftSubs(extension, progressive); } String blankLine = horizontal ? "" : SrtUtils.getBlankLine(); FileOutputStream fileOutputStream = new FileOutputStream(new File( fileOuput)); FileChannel fileChannel = fileOutputStream.getChannel(); Charset charset = Charset.forName(charsetStr); CharsetEncoder encoder = charset.newEncoder(); encoder.onMalformedInput(CodingErrorAction.IGNORE); encoder.onUnmappableCharacter(CodingErrorAction.IGNORE); CharBuffer uCharBuffer; ByteBuffer byteBuffer; if (horizontal) { for (String key : subtitles.keySet()) { Entry[] entries = subtitles.get(key); for (int j = 0; j < entries.length; j++) { List<String> subtitleLines = entries[j].getSubtitleLines(); String newLine = ""; for (String s : subtitleLines) { newLine += s + " "; } subtitleLines.clear(); subtitleLines.add(newLine.trim()); } } } String left = "", right = "", time = ""; String separator = SrtUtils.getSeparator().trim().isEmpty() ? SPACE_HTML : SrtUtils.getSeparator(); String horizontalSeparator = SrtUtils.EOL + (SrtUtils.isUsingSeparator() ? separator + SrtUtils.EOL : ""); for (int j = 0; j < subtitles.keySet().size(); j++) { time = (String) subtitles.keySet().toArray()[j]; byteBuffer = ByteBuffer.wrap((String.valueOf(j + 1) + SrtUtils.EOL) .getBytes(Charset.forName(charsetStr))); fileChannel.write(byteBuffer); byteBuffer = ByteBuffer.wrap((time + SrtUtils.EOL).getBytes(Charset .forName(charsetStr))); fileChannel.write(byteBuffer); int limit = subtitles.get(time).length > 1 ? Math.max( subtitles.get(time)[0].size(), subtitles.get(time)[1].size()) : subtitles.get(time)[0] .size(); for (int i = 0; i < limit; i++) { left = i < subtitles.get(time)[0].size() ? subtitles.get(time)[0] .get(i) : blankLine; if (subtitles.get(time).length > 1) { right = i < subtitles.get(time)[1].size() ? subtitles .get(time)[1].get(i) : blankLine; } String leftColor = SrtUtils.getParsedLeftColor(); if (leftColor != null) { left = String.format(leftColor, left); } String rightColor = SrtUtils.getParsedRightColor(); if (rightColor != null) { right = String.format(rightColor, right); } if (horizontal) { uCharBuffer = CharBuffer.wrap(left + horizontalSeparator + right + SrtUtils.EOL); } else { uCharBuffer = CharBuffer.wrap(left + right + SrtUtils.EOL); } byteBuffer = encoder.encode(uCharBuffer); log.debug(new String(byteBuffer.array(), Charset .forName(charsetStr))); fileChannel.write(byteBuffer); } byteBuffer = ByteBuffer.wrap(SrtUtils.EOL.getBytes(Charset .forName(charsetStr))); fileChannel.write(byteBuffer); } for (String s : signature(translate, merge)) { byteBuffer = ByteBuffer.wrap((s + SrtUtils.EOL).getBytes(Charset .forName(charsetStr))); fileChannel.write(byteBuffer); } byteBuffer = ByteBuffer.wrap(SrtUtils.EOL.getBytes(Charset .forName(charsetStr))); fileChannel.write(byteBuffer); fileChannel.close(); fileOutputStream.close(); } /** * Adds an entry in the end of the merged subtitles with the program author. * * @param subs * @throws ParseException */ private List<String> signature(boolean translate, boolean merge) throws ParseException { String lastEntryTime = (String) subtitles.keySet().toArray()[subtitles .keySet().size() - 1]; Date end = SrtUtils.getEndTime(lastEntryTime); final Date newDateInit = new Date(end.getTime() + signatureGap); final Date newDateEnd = new Date(end.getTime() + signatureTime); String newTime = SrtUtils.createSrtTime(newDateInit, newDateEnd); List<String> signature = new LinkedList<String>(); signature.add(String.valueOf(subtitles.size() + 1)); signature.add(newTime); String signatureText = ""; if (translate & merge) { signatureText = I18N.getText("Merger.signatureboth.text"); } else if (translate & !merge) { signatureText = I18N.getText("Merger.signaturetranslated.text"); } else { signatureText = I18N.getText("Merger.signature.text"); } signature.add(signatureText); signature.add(I18N.getText("Merger.signature.url")); return signature; } /** * This method extends the duration of each subtitle 1 second (EXTENSION). * If the following subtitle is located inside that extension, the extension * will be only until the beginning of this next subtitle minus 20 * milliseconds (GAP). * * @throws ParseException */ public void shiftSubs(int extension, boolean progressive) throws ParseException { TreeMap<String, Entry[]> newSubtitles = new TreeMap<String, Entry[]>(); String timeBefore = ""; Date init; Date tsBeforeInit = new Date(); Date tsBeforeEnd = new Date(); String newTime = ""; Entry[] entries; int shiftTime = extension; for (String t : subtitles.keySet()) { if (!timeBefore.isEmpty()) { init = SrtUtils.getInitTime(t); if (init != null) { entries = subtitles.get(timeBefore); if (progressive) { shiftTime = entries.length > 1 ? extension * Math.max(entries[0].size(), entries[1].size()) : entries[0].size(); } if (tsBeforeEnd.getTime() + shiftTime < init.getTime()) { newTime = SrtUtils.createSrtTime(tsBeforeInit, new Date(tsBeforeEnd.getTime() + shiftTime)); log.debug("Shift " + timeBefore + " to " + newTime + " ... extension " + shiftTime); } else { newTime = SrtUtils.createSrtTime(tsBeforeInit, new Date(init.getTime() - gap)); log.debug("Shift " + timeBefore + " to " + newTime); } newSubtitles.put(newTime, entries); } } timeBefore = t; tsBeforeInit = SrtUtils.getInitTime(timeBefore); tsBeforeEnd = SrtUtils.getEndTime(timeBefore); if (tsBeforeInit == null || tsBeforeEnd == null) { continue; } } // Last entry entries = subtitles.get(timeBefore); if (entries != null && tsBeforeInit != null && tsBeforeEnd != null) { if (progressive) { extension *= entries.length > 1 ? Math.max(entries[0].size(), entries[1].size()) : entries[0].size(); } newTime = SrtUtils.createSrtTime(tsBeforeInit, new Date(tsBeforeEnd.getTime() + extension)); newSubtitles.put(newTime, entries); } subtitles = newSubtitles; } public int size() { return subtitles.size(); } public int getDesync() { return desync; } }