1

I'm creating pyqt6 app where I can replace normal fonts/text with svg fonts(.svg files like a.svg, b.svg etc).

My issue is with word-wrap. In normal mode(svg mode is disabled) QTextEdit correctly moves a word to nex line if it does not fit completely in the line. But in svg mode it does not behave like that. It breaks word and pushes its some characters to the next line instead of moving the whole word to the next line.

I tried replacing space(' ') with '\u200B' or ' ' + '\u200B'. But nothing works. Enter works as expected.

Below is the relevant code:

NON_SVG_CHARS = {' ', '\t', '\u2029', '\r', '\n'}
class SvgTextObject(QObject, QTextObjectInterface):
    def __init__(self, char_path, parent=None):
        QObject.__init__(self, parent)
        QTextObjectInterface.__init__(self)
        self.char_path = char_path
        self.svg_renderer_cache = {}
        self.char_properties = {} 
        self.normalized_svg_sizes = {}

    def intrinsicSize(self, doc, pos, fmt):
  
        char_name = fmt.stringProperty(SVGSettings.SVG_TEXT_FORMAT)
       
        format_id = fmt.property(SVGSettings.SVG_INSTANCE_ID)
        properties = self.getProperties(format_id)
        
        normalized_size = self.getNormalizedSize(char_name)
        if not normalized_size.isValid():
            return QSizeF()

        font_size = properties.get(SVGSettings.PropertyStrings.FONT_SIZE, SVGSettings.FontSize.DEFAULT)
        letter_width_percent = properties.get(SVGSettings.PropertyStrings.LETTER_WIDTH, 100.0)
        letter_height_percent = properties.get(SVGSettings.PropertyStrings.LETTER_HEIGHT, 100.0)
        letter_spacing = properties.get(SVGSettings.PropertyStrings.LETTER_SPACING, 0.0)


        width_scale_factor = (100.0 + letter_width_percent) / 100.0
        height_scale_factor = (100.0 + letter_height_percent) / 100.0

  
        base_scale = font_size / normalized_size.height()
        width = normalized_size.width() * base_scale * width_scale_factor
        height = normalized_size.height() * base_scale * height_scale_factor
        
        #Add spacing to width for layout purposes
        if pos > 0:
            prev_char = doc.characterAt(pos - 1)
            if not prev_char.isspace() and prev_char != QChar('\n'):
                width += letter_spacing
        return QSizeF(width, height)


    def drawObject(self, painter: QPainter, rect, doc, pos, fmt):

        painter.save()
        
        char_name = fmt.stringProperty(SVGSettings.SVG_TEXT_FORMAT)
        format_id = fmt.property(SVGSettings.SVG_INSTANCE_ID)
        properties = self.getProperties(format_id)
        
        letter_slant_value = properties.get(SVGSettings.PropertyStrings.LETTER_SLANTING, 0.0)
        letter_base_offset = properties.get(SVGSettings.PropertyStrings.LETTER_BASE_OFFSET, 0.0)
        letter_spacing = properties.get(SVGSettings.PropertyStrings.LETTER_SPACING, 0.0)
        
        normalized_size = self.getNormalizedSize(char_name)
        if not normalized_size.isValid():
            painter.restore()
            return
            
        font_size = properties.get(SVGSettings.PropertyStrings.FONT_SIZE, SVGSettings.FontSize.DEFAULT)
        letter_width_percent = properties.get(SVGSettings.PropertyStrings.LETTER_WIDTH, 100.0)
        letter_height_percent = properties.get(SVGSettings.PropertyStrings.LETTER_HEIGHT, 100.0)

 
        width_scale_factor = (100.0 + letter_width_percent) / 100.0
        height_scale_factor = (100.0 + letter_height_percent) / 100.0
        
        # Calculate scales and offsets based on the font size and character properties
        base_scale = font_size / normalized_size.height()
        final_scale_x = base_scale * width_scale_factor
        final_scale_y = base_scale * height_scale_factor


        painter.translate(rect.x(), rect.y())
        

        current_x_offset = 0.0
        if pos > 0:
            prev_char = doc.characterAt(pos - 1)
            if not prev_char.isspace() and prev_char != QChar('\n'):
                current_x_offset += letter_spacing
        
        current_y_offset = letter_base_offset
        

        intrinsic_height = self.intrinsicSize(doc, pos, fmt).height()
        scaled_svg_height = normalized_size.height() * final_scale_y
        extra_height_space = intrinsic_height - scaled_svg_height
        current_y_offset += extra_height_space / 2
        
        painter.translate(current_x_offset, current_y_offset)


        if letter_slant_value != 0.0:
            transform = painter.transform()
            transform.shear(letter_slant_value / 100.0, 0)
            painter.setTransform(transform)

        painter.scale(final_scale_x, final_scale_y)
        
 
        renderer = self.svg_renderer_cache.get(char_name)
        if renderer and renderer.isValid():
            renderer.render(painter, QRectF(0, 0, normalized_size.width(), normalized_size.height()))
        
        painter.restore()
class CustomTextEdit(QGraphicsRectItem):

    def __init__(self, properties_provider=None, svg_font_directory=None, x=20, y=50, w=200, h=100):
        super().__init__(x, y, w, h)


        self.svg_font_directory = svg_font_directory
    
        self.svg_enabled = True
  

        self.text = EditableText(self, properties_provider)
        self.text.setReadOnly(True)
        self.text.pasteRequested.connect(self._handle_paste_request)
             
   
        self.text.document().contentsChange.connect(self.handle_contents_change)

        
        
        # self.text.textChanged.connect(self.handle_contents_change)
   
   
        # Register the custom SVG text object with the document's layout
        self.svg_object = SvgTextObject(self.svg_font_directory) # SVG directory
        self.text.document().documentLayout().registerHandler(SVGSettings.SVG_TEXT_FORMAT, self.svg_object)
        
        
    def handle_contents_change(self, position, chars_removed, chars_added):
        # check if replacing chars with svg chars is enabled
        if not self.svg_enabled:
            return
        
        if self.text._is_pasting:
            return
        
        # We only care about single-character additions
        if chars_removed == 0 and chars_added == 1:
            doc = self.text.document()
            new_char = doc.characterAt(position)

            if new_char in SVGSettings.NON_SVG_CHARS:
                return

            # already converted to unicode
            if new_char == '\uFFFC':
                return

            cursor = self.text.textCursor()
            self.text.document().blockSignals(True) # Block signals for safety
            
            cursor.beginEditBlock()

            cursor.setPosition(position)
            cursor.setPosition(position + 1, QTextCursor.MoveMode.KeepAnchor)
            
            svg_char_format = QTextCharFormat()
            svg_char_format.setObjectType(SVGSettings.SVG_TEXT_FORMAT)
            svg_char_format.setProperty(SVGSettings.SVG_TEXT_FORMAT, new_char)
            
            instance_id = str(uuid.uuid4())
            svg_char_format.setProperty(SVGSettings.SVG_INSTANCE_ID, instance_id)
            
            # Retrieve properties and set them before insertion
            properties = {}
            current_properties = self.text.properties_provider.get_current_properties()
            for prop_name, slider_values in current_properties.items():
                settings_class = SVGSettings.SLIDER_MAPPING.get(prop_name)
                if settings_class:
                    step_size = settings_class.STEP
                    properties[prop_name] = get_random_value_with_step(slider_values, step_size)
                else:
                    properties[prop_name] = slider_values
            
            self.svg_object.setRandomProperties(instance_id, properties)

            cursor.insertText("\uFFFC", svg_char_format)

            cursor.endEditBlock()
            self.text.document().blockSignals(False) # Unblock signals
8
  • 2
    I'm not 100% sure, but I doubt it would be possible: in Qt, non-text objects are considered as word boundaries, therefore, similarly to images, they are always wrapped if there's not enough space before or after them. The limited capabilities don't support the display property of CSS, probably because the common text document layout simply doesn't allow such possibility. The only way to achieve this with QTextDocument would be to create your own QAbstractTextDocumentLayout subclass (which is quite complex). Consider using the QtWebEngine module. Commented Sep 5 at 21:41
  • That will be way too complex for my level of knowledge. Is there any other simpler 'hackish' way? Commented Sep 6 at 4:59
  • Well, in theory you could try to "merge" adjacent SVGs into a single object, the only problem is managing their proper insertion/deletion. For insertion, you should then check if the character before the current position is already a QTextObjectInterface, and then modify its properties according to the new insertion (I don't fully remember, but since contentsChange is emitted before the layout is notified, then it should be fine). The real problem is detecting the deletion, for which you may need to filter keyboard events (backspace/delete or Ctrl-X) or the context menu actions. Commented Sep 6 at 17:49
  • Note that I'm not very convinced about completely blocking signals of the document. If you're doing it to avoid recursion on the calls of handle_contents_change(), then use a simple bool flag: check it at the beginning of the function, if it's set, then return immediately, otherwise set it right before making any change, and unset it at the end of the process (or whenever you return). If you still want to block signals, it's preferable to use QSignalBlocker, which is safer to use. Commented Sep 6 at 17:55
  • 1
    @musicamante, assuming that using text instead is possible, and wrapping would work with a text object, the alternative is to create an opentype font using the svg files for glyphs in the font, which would mean you would be working with a more standard text object, assuming the rendering engine can handle svg flavoured opentype fonts. Commented Sep 8 at 23:34

0

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.