fons
clone your own copy | download snapshot

Snapshots | iceberg

Inside this repository

glyphtracer
text/x-python

Download raw (16.4 KB)

#!/usr/bin/python -tt
# -*- coding: utf-8 -*-

#    Glyphtracer
#    Copyright (C) 2010 Jussi Pakkanen
#    version 1.4 (c) 2015 Stéphanie Vilayphiou
#
#    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/>.

import os, sys
import PyQt5.QtWidgets as QtWidgets
import PyQt5.QtCore as QtCore
import PyQt5.QtGui as QtGui
from gtlib import *

start_dialog = None
main_win = None
app = None

def QString(x):
  return x

def calculate_horizontal_sums(image, show_progress):
    (w, h) = (image.width(), image.height())
    if show_progress:
        prog = QtWidgets.QProgressDialog("Vertical splitting", '', 0, h)
        prog.show()
    sums = []
    for j in range(h):
        total = 0
        for i in range(w):
            total += image.pixelIndex(i, j)
        sums.append(total)
        if show_progress:
            prog.setValue(j)
    if show_progress:
        prog.hide()
    return sums
    
def calculate_cutlines_locations(sums):
    element_strips = []
    cutoff = 0
    
    if len(sums) == 0:
        return []
    if sums[0] <= cutoff:
        background_strip = True
    else:
        background_strip = False
    strip_start = 0
    
    for i in range(len(sums)):
        if sums[i] <= cutoff:
            background = True
        else:
            background = False
        if background == background_strip:
            continue
        
        # We crossed a region.
        if background:
           strip_end = i-1;
           element_strips.append((strip_start, strip_end))
        strip_start = i
        background_strip = background
          
    if strip_start < len(sums) and not background_strip:
        strip_end = len(sums) - 1
        element_strips.append((strip_start, strip_end))
    return element_strips

def calculate_letter_boxes(image, xstrips):
    boxes = []
    (w, h) = (image.width(), image.height())
    rotate = QtGui.QTransform()
    rotate.rotate(90)
    prog = QtWidgets.QProgressDialog("Horizontal splitting", '', 0, len(xstrips))
    prog.show()
    stripnum = 0
    for xs in xstrips:
        (y0, y1) = xs
        cur_image = image.copy(0, y0, w, y1-y0).transformed(rotate)
        ystrips = calculate_cutlines_locations(calculate_horizontal_sums(cur_image, False))
        for ys in ystrips:
            (x0, x1) = ys
            box = LetterBox(QtCore.QRect(x0, y0, x1-x0, y1-y0))
            boxes.append(box)
        prog.setValue(stripnum)
        stripnum += 1
    prog.hide()
    return boxes

class SelectionArea(QtWidgets.QWidget):
    def __init__(self, image, master_widget, parent = None):
        super().__init__(parent)
        self.master = master_widget
        self.original_image = image
        self.set_zoom(1)
        
        strips = calculate_horizontal_sums(self.image, True)
        hor_lines = calculate_cutlines_locations(strips)
        self.boxes = calculate_letter_boxes(self.image, hor_lines)
        self.active_box = None

        self.selected_brush = QtGui.QBrush(QtGui.QColor(0, 0, 0, 127))
        self.active_brush = QtGui.QBrush(QtGui.QColor(255, 255, 0, 127))

    def set_zoom(self, value):
        self.zoom = value
        self.build_zoom_image()
        self.resize(self.image.width(), self.image.height())

    def build_zoom_image(self):
        if self.zoom == 1:
            self.image = self.original_image
        else:
            w = self.original_image.width()
            self.image = self.original_image.scaledToWidth(w/self.zoom)

    def paintEvent(self, event):
        paint = QtGui.QPainter()
        paint.begin(self)
        paint.drawImage(0, 0, self.image)
        pen = QtGui.QPen(QtCore.Qt.black, 1, QtCore.Qt.SolidLine)
        paint.setPen(pen)
        for box in self.boxes:
            zoomed_box = self.scale_box(box.r)
            paint.drawRect(zoomed_box)
            if box is self.active_box:
                paint.fillRect(zoomed_box, self.active_brush)
            elif box.taken:
                paint.fillRect(zoomed_box, self.selected_brush)
        paint.end()
        
    def scale_box(self, box):
        size = box.size()
        p = box.topLeft()
        scaled_size = QtCore.QSize(int(size.width()/self.zoom), int(size.height()/self.zoom))
        scaled_p = QtCore.QPoint(int(p.x()/self.zoom), int(p.y()/self.zoom))
        return QtCore.QRect(scaled_p, scaled_size)

    def find_box(self, unscaled_x, unscaled_y):
        x = unscaled_x*self.zoom
        y = unscaled_y*self.zoom
        for b in self.boxes:
            if b.contains(x, y):
                return b
        return None
    
    def set_active_box(self, box):
        self.active_box = box
        
    def take_box(self, box):
        box.taken = True
    
    def mousePressEvent(self, me):
        # I could not figure out how to connect
        # to events in some other widget, so
        # we have this ugly hack.
        self.master.user_click(me)

class StartDialog(QtWidgets.QWidget):
    def __init__(self, initial_file_name=None):
        super().__init__()
        self.resize(512, 200)
        
        self.grid = QtWidgets.QGridLayout()
        self.name_edit = QtWidgets.QLineEdit('MyFont')
        self.font_height_edit = QtWidgets.QLineEdit('1000')
        self.font_ascent_edit = QtWidgets.QLineEdit('780')
        self.file_edit = QtWidgets.QLineEdit()
        self.output_edit = QtWidgets.QLineEdit()
        if initial_file_name is not None:
            self.file_edit.setText(initial_file_name)
            self.set_output_file_from_source(initial_file_name)
        self.file_button = QtWidgets.QPushButton('Browse')
        self.file_button.clicked.connect(self.open_file)

        self.grid.setSpacing(10)
        self.grid.addWidget(QtWidgets.QLabel('Font name'), 0, 0)
        self.grid.addWidget(self.name_edit, 0, 1, 1, 3)
        
        self.grid.addWidget(QtWidgets.QLabel('Font height'), 1, 0)
        self.grid.addWidget(self.font_height_edit, 1, 1)
        
        self.grid.addWidget(QtWidgets.QLabel('Font ascent'), 1, 2)
        self.grid.addWidget(self.font_ascent_edit, 1, 3)

        self.grid.addWidget(QtWidgets.QLabel('Image file'), 2, 0)
        self.grid.addWidget(self.file_edit, 2, 1, 1, 2)
        self.grid.addWidget(self.file_button, 2, 3)

        self.grid.addWidget(QtWidgets.QLabel('Output file'), 3, 0)
        self.grid.addWidget(self.output_edit, 3, 1, 1, 3)
        
        hbox = QtWidgets.QHBoxLayout()
        about_button = QtWidgets.QPushButton('About')
        about_button.clicked.connect(self.about_message)
        hbox.addWidget(about_button)
        start_button = QtWidgets.QPushButton('Start')
        start_button.clicked.connect(self.start_edit)
        hbox.addWidget(start_button)
        quit_button = QtWidgets.QPushButton('Quit')
        quit_button.clicked.connect(self.quit_app)
        hbox.addWidget(quit_button)
        w = QtWidgets.QWidget()
        w.setLayout(hbox)
        self.grid.addWidget(w, 4, 0, 1, 3)
        
        self.setLayout(self.grid)

    def quit_app(self):
        global app
        app.quit()

    def open_file(self):
        fname = QtWidgets.QFileDialog.getOpenFileName(self)[0]
        if fname is not None and fname != '':
            self.file_edit.setText(fname)
            self.set_output_file_from_source(fname)
    
    def set_output_file_from_source(self, name):
        parts = str(name).split('.')
        if len(parts) > 1:
            parts = parts[:-1]
        parts.append('sfd')
        self.output_edit.setText('.'.join(parts))
    
    def about_message(self):
        QtWidgets.QMessageBox.information(self, "About " + program_name, 
                                program_name + ' ' + program_version + \
                                "\n(C) 2010 Jussi Pakkanen, and (c) 2015 Stéphanie Vilayphiou.\nThis program is available under the Gnu General Public License v3 or later.")
    
    def does_file_exist(self, fname):
        try:
            f = open(fname, 'r')
            return True
        except IOError:
            return False

    def is_image_file_valid(self, im):
        if im.isNull() or im.depth() != 1:
            return False
        return True

    def start_edit(self):
        global main_win, start_dialog
        fname = self.file_edit.text()
        output = self.output_edit.text()
        font_name = self.name_edit.text()
        font_height = self.font_height_edit.text()
        font_ascent = self.font_ascent_edit.text()
        image = QtGui.QImage(fname)
        if not self.is_image_file_valid(image):
            QtWidgets.QMessageBox.critical(self,
                                           "Error",
                                           "Selected file is not a 1 bit image.")
            return
        if image.hasAlphaChannel():
            QtWidgets.QMessageBox.critical(self,
                                           "Error",
                                           "The image has transparency, which will interfere with tracing. Remove alpha channel and try again.")
            return
        if self.does_file_exist(output):
            if QtWidgets.QMessageBox.critical(self,
                                              "File exists",
                                              "Output file %s already exists, overwrite?" % output,
                                              QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) == QtWidgets.QMessageBox.No:
                return
        start_dialog.hide()
        main_win = EditorWindow(image, font_name, font_height, font_ascent, output)
        main_win.show()

class EditorWindow(QtWidgets.QWidget):
    def __init__(self, image, font_name, font_height, font_ascent, sfd_file, parent=None):
        super().__init__()
        self.active_glyph = 0
        self.glyphlist = []
        self.font_name = font_name
        self.font_height = font_height
        self.font_ascent = font_ascent
        self.sfd_file = sfd_file

        self.set_nice_windowsize(image)
        
        self.setWindowTitle(program_name + ': ' + font_name)
        
        self.grid = QtWidgets.QGridLayout()
        self.area = SelectionArea(image, self)
        sa = QtWidgets.QScrollArea()
        sa.setWidget(self.area)
        self.grid.addWidget(sa, 0, 0, 1, 6)
        
        b = QtWidgets.QPushButton('Previous glyph')
        b.clicked.connect(self.previous_button)
        self.grid.addWidget(b, 1, 1, 1, 1)
        b = QtWidgets.QPushButton('Next glyph')
        b.clicked.connect(self.next_button)
        self.grid.addWidget(b, 1, 2, 1, 1)
        
        self.glyph_text = QtWidgets.QLabel('Glyph:')
        self.glyph_text.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Fixed)
        self.grid.addWidget(self.glyph_text, 1, 3, 1, 1)
        self.save = QtWidgets.QPushButton('Generate SFD file')
        self.save.clicked.connect(self.generate_sfd)
        self.grid.addWidget(self.save, 1, 5, 1, 1)
        
        self.combo = QtWidgets.QComboBox()
        self.build_glyph_combo()
        self.combo.activated.connect(self.glyph_set_changed)
        self.grid.addWidget(self.combo, 1, 0, 1, 1)

        self.zoomlevel = QtWidgets.QSpinBox()
        self.zoomlevel.setMaximum(30)
        self.zoomlevel.setMinimum(1)
        self.zoomlevel.setSingleStep(1)
        self.zoomlevel.setPrefix('Zoom level: ')
        self.zoomlevel.valueChanged.connect(self.zoom_changed)
        self.grid.addWidget(self.zoomlevel, 1, 4, 1, 1)
        
        self.setLayout(self.grid)
        
    def set_nice_windowsize(self, image):
        global app
        w = image.width()
        h = image.height()
        desk = app.desktop()
        rect = desk.screenGeometry(desk.primaryScreen())
        screen_width = rect.width()
        screen_height = rect.height()
        final_width = min(0.9*screen_width, w+50)
        final_height = min(0.9*screen_height, h+100)
        self.resize(final_width, final_height)
        
    def build_glyph_combo(self):
        self.groups = {}
        for name, glyphs in glyph_groups:
            self.groups[name] = [data_to_glyphinfo(x) for x in glyphs]
            self.combo.addItem(name)
        self.glyph_set_changed(0)
    
    def user_click(self, mouse_event):
        (x, y) = (mouse_event.x(), mouse_event.y())
        newbox = self.area.find_box(x, y)
        if newbox:
            self.unselect(newbox)
            self.area.take_box(newbox)
            oldbox = self.glyphlist[self.active_glyph].box
            if oldbox is not None:
                oldbox.taken = False
            self.glyphlist[self.active_glyph].box = newbox
            self.go_to_next_glyph()

    def keyPressEvent(self, key_event):
        if key_event.key() == QtCore.Qt.Key_Space:
            forward = True
        else:
            forward = False
        self.go_to_next_glyph(forward)
        
    def next_button(self):
        self.go_to_next_glyph(True)
    
    def previous_button(self):
        self.go_to_next_glyph(False)
        
    def go_to_next_glyph(self, forward=True):
        if forward:
            shift = 1
        else:
            shift = -1
        gs = len(self.glyphlist)
        self.active_glyph = (self.active_glyph + shift + gs) % gs
        self.glyph_info_changed()
        
    def glyph_set_changed(self, i):
        self.active_glyph = 0
        self.glyphlist = self.groups[str(self.combo.currentText())]
        self.glyph_info_changed()

    def glyph_info_changed(self):
        self.set_glyph_info()
        self.area.set_active_box(self.glyphlist[self.active_glyph].box)
        self.area.repaint()
        
    def zoom_changed(self, value):
        self.area.set_zoom(value)
        self.area.repaint()
        
    def set_glyph_info(self):
        g = self.glyphlist[self.active_glyph]
        info_text = u'Glyph %d/%d: %s (%s)' % \
        (self.active_glyph+1, len(self.glyphlist), str(g.name), chr(g.codepoint))
        self.glyph_text.setText(info_text)
        
    def unselect(self, box):
        box.taken = False
        for name in self.groups.keys():
            for g in self.groups[name]:
                if g.box is box:
                    g.box = None
                    return
        
    def get_selected_glyphs(self):
        selected = []
        for name in self.groups.keys():
            selected += filter(lambda x: x.box is not None, self.groups[name])
        return selected
        
    def generate_sfd(self):
        self.area.set_zoom(1)
        self.area.repaint()
        selected = self.get_selected_glyphs()
        if len(selected) == 0:
            QtWidgets.QMessageBox.critical(self, "Error", "No glyphs selected, can not generate sfd file.\n")
            return
        try:
            print(self.sfd_file, self.font_name, self.area.image, selected, self.font_height, self.font_ascent)
            write_sfd(self.sfd_file, self.font_name, self.area.image, selected, self.font_height, self.font_ascent)

        except Exception as e:
            print(e)
            QtWidgets.QMessageBox.critical(self, "Error", "SFD generation failed:\n" + str(e))
            return
            
        QtWidgets.QMessageBox.information(self, "Success", "Sfd file successfully generated.")
        
def start_program(arguments):
    global start_dialog, app
    app = QtWidgets.QApplication(arguments)
    #if not i_haz_potrace():
    #    QMessageBox.critical(None, program_name, "Potrace executable not in path, exiting.")
    #    sys.exit(127)
    if len(arguments) > 1:
        start_dialog = StartDialog(arguments[1])
    else:
        start_dialog = StartDialog()
    start_dialog.setWindowTitle(program_name)
    start_dialog.show()
    sys.exit(app.exec_())

def test_edwin():
    global app
    app = QtWidgets.QApplication(sys.argv)
    bob = EditorWindow(sys.argv[1], 'temporary_out.sfd', 'MyFont')
    bob.show()
    sys.exit(app.exec_())
    
def test_progress():
    import time
    app = QtWidgets.QApplication(sys.argv)
    prog = QtWidgets.QProgressDialog("Testing progressbar", '', 0, 100)
    prog.show()
    for i in range(100):
        time.sleep(0.1)
        prog.setValue(i)
    
if __name__ == "__main__":
    start_program(sys.argv)
    #test_progress()
    #test_edwin()