Skip to content

Commit

Permalink
Add font recognition and exporting
Browse files Browse the repository at this point in the history
  • Loading branch information
naghim committed May 13, 2024
1 parent e04d86c commit abf618d
Show file tree
Hide file tree
Showing 11 changed files with 291 additions and 11 deletions.
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# <p align='center'>{ Sub<span style="color: #d9073f">Assistant }</span></p>
<p align="center">
<img width="500" src="https://i.imgur.com/ocW7Hse.png"" alt="SubAssistant screenshot"/>
</p>

<p align='justify'>This is a specialized desktop application designed to simplify the translation process for subtitle files, specifically for the <code>.ass</code> (Advanced SubStation Alpha) format. Tailored for translators, SubAssistant facilitates seamless collaboration by allowing users to comment out the original dialogue text, write their translations alongside it, and enable proofreaders or quality checkers to review both versions within the same file. Users also have the possibilitiy to delete the commented out texts, doing so the application enhances the efficiency and accuracy of subtitle translation workflows.</p>
<p align='justify'>This is a specialized desktop application designed to simplify the translation process for subtitle files, specifically for the <code>.ass</code> (Advanced SubStation Alpha) format. Tailored for translators, SubAssistant facilitates seamless collaboration by allowing users to comment out the original dialogue text, write their translations alongside it, and enable proofreaders or quality checkers to review both versions within the same file. Users also have the possibilitiy to delete the commented out texts, by doing so the application enhances the efficiency and accuracy of subtitle translation workflows. SubAssistant also aids in recognizing fonts from an <code>.ass</code> subtitle file that are not installed on your machine, also giving the opportunity to export all fonts used in a subtitle.</p>

<p align="center">
<img width="500" src="https://i.imgur.com/vvzOFnF.png"" alt="SubAssistant screenshot"/>
<img width="500" src="https://i.imgur.com/Kpbfsmj.png"" alt="SubAssistant screenshot"/>
</p>

## Installation
Expand Down Expand Up @@ -52,7 +54,7 @@ python -m subassistant

## FAQ

<p align='justify'><b> 👀 How to use SubAssistant?</b> </br> Using SubAssistant is a piece of cake! Choose the action you wish to take from the side menu—whether to comment out text or delete comments. Use the "Select File" button to open the .ass file. The program will automatically propose an output filename within the same folder. If you prefer a different folder or wish to rename the output, utilize the "Browse" button or directly edit the output path. Upon clicking the button, the selected operation will be executed.</p>
<p align='justify'><b> 👀 How to use SubAssistant?</b> </br> Using SubAssistant is a piece of cake! Choose the action you wish to take from the side menu—whether to comment out text or delete comments. Use the "Select File" button to open the .ass file. The program will automatically propose an output filename within the same folder. If you prefer a different folder or wish to rename the output, utilize the "Browse" button or directly edit the output path. Upon clicking the button, the selected operation will be executed. To pinpoint missing fonts, begin by selecting the subtitle, then click the "Check Fonts in System" button. The application will display a list of fonts from the subtitle, indicating installed fonts along with their locations on your machine. If all fonts are installed, you can also export them into a folder by clicking the "Export Fonts to Folder" button.</p>

<p align='justify'><b> 👀 Can SubAssistant mess up my subtitles?</b> </br> No, SubAssistant will always generate a new file with the modifications, does not do any editing in the input file, so your subtitles are safe!</p>

Expand Down
4 changes: 3 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
pyside6
pyside6
ass
fonttools
155 changes: 151 additions & 4 deletions subassistant/gui/tab.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QPushButton, QFileDialog, QHBoxLayout, QLineEdit, QScrollArea, QMessageBox
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QPushButton, QFileDialog, QHBoxLayout, QLineEdit, QScrollArea, QMessageBox, QTableWidget, QHeaderView, QTableWidgetItem, QCheckBox
from PySide6.QtCore import Qt
from PySide6.QtGui import QColor
from subassistant.logic.logic import RemoveComments, CommentDialogue
from subassistant.gui import util
from subassistant.logic.fonts import copy, font, subtitle, windows
import os

class BaseSubtitleUI(QWidget):
Expand Down Expand Up @@ -114,6 +116,151 @@ class RemoveCommentTab(BaseSubtitleUI):
ACTION_CLASS = RemoveComments



class FontCheckerTab(QWidget):
TAB_TITLE = "Font Checker"

def __init__(self):
super().__init__()
self.subtitle_filename = None

self.layout = QVBoxLayout()
self.window_title = QLabel(self.TAB_TITLE)
self.window_title.setObjectName("Label_txt_bold")
self.layout.addWidget(self.window_title)

self.subtitle_label = QLabel("Select subtitle:")
self.subtitle_label.setObjectName("Label_txt")
self.layout.addWidget(self.subtitle_label)

self.subtitle_line_edit = QLineEdit()
self.subtitle_line_edit.setReadOnly(True)
self.subtitle_line_edit.setObjectName("LineEdit")

self.input_btn = QPushButton("Browse", clicked=self.get_subtitle)
self.input_btn.setObjectName("FileChooserButton")

self.subtitle_layout = QHBoxLayout()
self.subtitle_layout.addWidget(self.subtitle_line_edit)
self.subtitle_layout.addWidget(self.input_btn)

self.layout.addLayout(self.subtitle_layout)


self.table_widget = QTableWidget()
self.table_widget.setColumnCount(3)
self.table_widget.setHorizontalHeaderLabels(["Font", "Installed", "Location"])
self.table_widget.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch)
self.table_widget.horizontalHeader().setSectionResizeMode(2, QHeaderView.Stretch)
self.table_widget.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeToContents)
self.table_widget.setObjectName("TableWidget")
self.table_widget.setStyleSheet("QTableWidget#TableWidget {"
"border: 1px solid #dcdcdc;"
"border-radius: 5px;"
"}")
self.layout.addWidget(self.table_widget)

self.button_widget = QWidget()
self.button_widget.setContentsMargins(0, 0, 0, 0)

self.button_layout = QHBoxLayout(self.button_widget)
self.button_layout.setContentsMargins(0, 0, 0, 0)

self.check_button = QPushButton("Check Fonts in System")
self.check_button.setObjectName("Action_btn")
self.check_button.clicked.connect(lambda: self.check_fonts())
self.check_button.setEnabled(False)

self.export_button = QPushButton("Export Fonts to Folder")
self.export_button.setObjectName("Action_btn")
self.export_button.clicked.connect(lambda: self.export_fonts())
self.export_button.setDisabled(True)

self.button_layout.addWidget(self.check_button)
self.button_layout.addWidget(self.export_button)

util.apply_background_color(self, QColor(249, 249, 249))
self.layout.addWidget(self.button_widget)
self.setLayout(self.layout)

def get_subtitle(self):
filename, _ = QFileDialog.getOpenFileName(self, "Select Subtitle", filter="Subtitles (*.ass)")

if not filename:
return

self.subtitle_line_edit.setText(os.path.basename(filename))
self.subtitle_filename = filename
self.check_button.setEnabled(True)

def add_fonts(self, fonts, installed):
for font, font_filename in fonts:
self.table_widget.insertRow(self.table_widget.rowCount())
row = self.table_widget.rowCount() - 1

font_item = QTableWidgetItem(font)

cell_widget = QWidget()
checkbox = QCheckBox()
checkbox.setCheckState(Qt.Checked if installed else Qt.Unchecked)
checkbox.setEnabled(False)
checkbox_layout = QHBoxLayout(cell_widget)
checkbox_layout.addWidget(checkbox)
checkbox_layout.setAlignment(Qt.AlignCenter)
checkbox_layout.setContentsMargins(0, 0, 0, 0)
cell_widget.setLayout(checkbox_layout)

filename_item = QTableWidgetItem(font_filename)
filename_item.setToolTip(font_filename)

self.table_widget.setItem(row, 0, font_item)
self.table_widget.setCellWidget(row, 1, cell_widget)
self.table_widget.setItem(row, 2, filename_item)
self.table_widget.setWordWrap(False)

def check_fonts(self):
if not self.subtitle_filename:
return

self.check_button.setEnabled(False)

# First, find all installed fonts...
installed_font_ttfs = windows.find_installed_ttfs()

# Second, find the font names of all installed fonts...
installed_fonts = {}
font.parse_font_names(installed_font_ttfs, installed_fonts)

# Third, find the fonts in the subtitle...
subtitle_fonts = subtitle.find_fonts_in_subtitle(self.subtitle_filename)

# Fourth, check which fonts are installed...
subtitle_fonts = [(font, installed_fonts.get(font, 'N/A')) for font in subtitle_fonts]
self.all_installed_fonts = [font for font in subtitle_fonts if font[0] in installed_fonts]
all_unavailable_fonts = [font for font in subtitle_fonts if font[0] not in installed_fonts]

# Fifth, update the table...
self.table_widget.setRowCount(0)

self.add_fonts(all_unavailable_fonts, False)
self.add_fonts(self.all_installed_fonts, True)

# Sixth, enable the export button if there are fonts to export...
self.export_button.setEnabled(not all_unavailable_fonts)
self.check_button.setEnabled(True)

def export_fonts(self):
output_folder = QFileDialog.getExistingDirectory(self, "Select Folder to save fonts in")

if not output_folder:
return

try:
copy.copy_fonts(output_folder, self.all_installed_fonts)
QMessageBox.information(self, "Success", "Fonts exported successfully.")
except:
QMessageBox.critical(self, "Error", "An error occurred while exporting fonts.")

class AboutTab(QWidget):
def __init__(self):
super().__init__()
Expand All @@ -132,13 +279,13 @@ def initUI(self):
self.layout.addWidget(scroll_area)


self.text_logo = QLabel("<html><head/><body><p align='center'>Sub<span style=\"color: #d9073f\">Assistant</span></p></body></html>"
self.text_logo = QLabel("<html><head/><body><h3 align='center'>Sub<span style=\"color: #d9073f\">Assistant</span></h3></body></html>"
)

self.text = QLabel("<html><head/><body><p align='justify'><b> 👀 What is this?</b> SubAssistant is a specialized desktop application designed to simplify the translation process for subtitle files, specifically for the .ass (Advanced SubStation Alpha) format. Tailored for translators, SubAssistant facilitates seamless collaboration by allowing users to comment out the original dialogue text, write their translations alongside it, and enable proofreaders or quality checkers to review both versions within the same file. Users also have the possibilitiy to delete the commented out texts, doing so the application enhances the efficiency and accuracy of subtitle translation workflows.</p></body></html>"
self.text = QLabel("<html><head/><body><p align='justify'><b> 👀 What is this?</b> SubAssistant is a specialized desktop application designed to simplify the translation process for subtitle files, specifically for the .ass (Advanced SubStation Alpha) format. Tailored for translators, SubAssistant facilitates seamless collaboration by allowing users to comment out the original dialogue text, write their translations alongside it, and enable proofreaders or quality checkers to review both versions within the same file. Users also have the possibilitiy to delete the commented out texts, by doing so the application enhances the efficiency and accuracy of subtitle translation workflows.<br/> SubAssistant also aids in recognizing fonts from an .ass subtitle file that are not installed on your machine, also giving the opportunity to export all fonts used in a subtitle.</p></body></html>"
)

self.text_how_to = QLabel("<html><head/><body><p align='justify'><b> 👀 How to use?</b> Choose the action you wish to take from the side menu—whether to comment out text or delete comments. Use the \"Select File\" button to open the .ass file. The program will automatically propose an output filename within the same folder. If you prefer a different folder or wish to rename the output, utilize the \"Browse\" button or directly edit the output path. Upon clicking the button, the selected operation will be executed.</p></body></html>"
self.text_how_to = QLabel("<html><head/><body><p align='justify'><b> 👀 How to use?</b> Choose the action you wish to take from the side menu—whether to comment out text or delete comments. Use the \"Select File\" button to open the .ass file. The program will automatically propose an output filename within the same folder. If you prefer a different folder or wish to rename the output, utilize the \"Browse\" button or directly edit the output path. Upon clicking the button, the selected operation will be executed.<br/> To pinpoint missing fonts, begin by selecting the subtitle, then click the \"Check Fonts in System\" button. The application will display a list of fonts from the subtitle, indicating installed fonts along with their locations on your machine. If all fonts are installed, you can also export them into a folder by clicking the \"Export Fonts to Folder\" button.</p></body></html>"
)

self.text_slogan = QLabel("<html><head/><body><p align='center'>Made with 50% 💕 and 50% ☕ <br/> by <a style=\"color: #d9073f\" href=\"https://github.com/naghim\">naghim @ GitHub</a></p></body></html>"
Expand Down
3 changes: 2 additions & 1 deletion subassistant/gui/window.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from PySide6.QtGui import QIcon, QColor
from PySide6.QtWidgets import QTabWidget, QWidget, QVBoxLayout
from subassistant.globals import RESOURCES_DIR
from subassistant.gui.tab import CommentTab, RemoveCommentTab, AboutTab
from subassistant.gui.tab import CommentTab, RemoveCommentTab, FontCheckerTab, AboutTab
from subassistant.gui import util
import os

Expand All @@ -22,6 +22,7 @@ def initUI(self):

self.tab_widget.addTab(CommentTab(), QIcon(os.path.join(RESOURCES_DIR, 'curly_braces.png')), "")
self.tab_widget.addTab(RemoveCommentTab(), QIcon(os.path.join(RESOURCES_DIR, 'no_curly_braces.png')), "")
self.tab_widget.addTab(FontCheckerTab(), QIcon(os.path.join(RESOURCES_DIR, 'font_icon.png')), "")
self.tab_widget.addTab(AboutTab(), QIcon(os.path.join(RESOURCES_DIR, 'about.png')), "")

self.tab_widget.setObjectName("TabWidget")
Expand Down
Empty file.
26 changes: 26 additions & 0 deletions subassistant/logic/fonts/copy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import shutil
import os

def copy_fonts(font_folder, subtitle_fonts):
# Create the folder if it does not exist
if not os.path.exists(font_folder):
os.makedirs(font_folder)

# Make sure not to copy fonts twice
fonts_copied = set()

for font, installed_ttf in subtitle_fonts:
if installed_ttf in fonts_copied:
# This font was already copied
continue

extension = os.path.splitext(installed_ttf)[-1]
font_file = os.path.join(font_folder, f'{font}{extension}')

if os.path.exists(font_file):
# This font already exists in the folder
continue

# Copy the font into the folder
shutil.copyfile(installed_ttf, font_file)
fonts_copied.add(installed_ttf)
41 changes: 41 additions & 0 deletions subassistant/logic/fonts/font.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from fontTools import ttLib
from fontTools.ttLib import TTLibError

def parse_font_name(ttf, installed_fonts):
# Some fonts are collections
# For these fonts, we have to query each font separately
fontNumber = 0

while True:
try:
font = ttLib.TTFont(ttf, fontNumber=fontNumber)
except TTLibError as e:
# Maybe this isn't a TrueType font?
if 'Not a TrueType' in str(e):
print(ttf, 'is not a TrueType font')
break

# Let's check all names.
for name in font['name'].names:
# https://learn.microsoft.com/en-us/typography/opentype/spec/name#name-ids
# Only check names that are font names
if name.nameID in [1, 4]:
# For some reason, names are in UTF-16-BE
try:
name = name.string.decode('utf-16-be')
except:
name = name.string.decode('utf-8')

installed_fonts[name] = ttf

if font.sfntVersion != b'ttcf':
# Not a collection, so let's not check the rest of the fonts
break

# Check the next font if this is a collection
fontNumber += 1

def parse_font_names(ttfs, installed_fonts):
# Read all font files and see which are installed
for ttf in ttfs:
parse_font_name(ttf, installed_fonts)
33 changes: 33 additions & 0 deletions subassistant/logic/fonts/subtitle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import ass
import re

def find_fonts_in_subtitle(subtitle_filename):
# Now let's parse the subtitles
with open(subtitle_filename, 'r', encoding='utf-8-sig') as f:
doc = ass.parse(f)

# These are the fonts that are used in the subtitle
fonts = set()

# First, the fonts used in all styles...
for style in doc.styles:
font_name = style.fontname
fonts.add(font_name)

# Second, the fonts that are manually specified in each dialogue
# Format: \fnFontName\
pattern = re.compile(r'\\fn([^\\}]+)(\\|})')

for event in doc.events:
fonts_found = pattern.findall(event.text)

# Add all fonts that are found
for font in fonts_found:
# The first match is the font name itself
# The second match is the delimiter
fonts.add(font[0])

# Sort the fonts
fonts = list(fonts)
fonts = sorted(fonts)
return fonts
23 changes: 23 additions & 0 deletions subassistant/logic/fonts/windows.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import os
import winreg

def find_installed_ttfs():
installed_font_ttfs = []

# Find all installed font TTFs
# There are two places to check:
# - System fonts (Windows)
# - User installed fonts (AppData)
for registry_hive in [winreg.HKEY_LOCAL_MACHINE, winreg.HKEY_CURRENT_USER]:
reg = winreg.ConnectRegistry(None, registry_hive)
key = winreg.OpenKey(reg, r'SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts', 0, winreg.KEY_READ)

for i in range(0, winreg.QueryInfoKey(key)[1]):
ttf = winreg.EnumValue(key, i)[1]

if '\\' not in ttf:
ttf = os.path.join(os.environ['WINDIR'], 'Fonts', ttf)

installed_font_ttfs.append(ttf)

return installed_font_ttfs
Binary file added subassistant/resources/font_icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 6 additions & 1 deletion subassistant/resources/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,16 @@
padding: 10px 20px;
text-align: center;
text-decoration: none;
font-size: 16px;
font-size: 15px;
margin: 4px 2px;
border-radius: 8px;
}

#Action_btn:disabled {
background-color: #d7d7d7;
color: #000000;
}

#Label_txt {
color: #333;
font-size: 16px;
Expand Down

0 comments on commit abf618d

Please sign in to comment.