WeasyPrint icon indicating copy to clipboard operation
WeasyPrint copied to clipboard

Preventing text selection does not work with user-select: none

Open solhewe opened this issue 5 years ago • 7 comments

Hello!

I have some text and I want to prevent it from being selected/highlighted. I tried to use user-select: none, but it's ignored as unknown property. Is there any way around solutions for preventing text selection in Weasyprint?

Thanks!

solhewe avatar Jun 10 '20 07:06 solhewe

Hello! Has there been any progress at all with this feature? Are there any blockers that need solving before this can be implemented?

nnsee avatar Sep 24 '24 14:09 nnsee

Hello! Has there been any progress at all with this feature? Are there any blockers that need solving before this can be implemented?

Hi!

There’s no progress yet on this topic. What’s blocking is that we miss time to take care of all the open issues!

To solve this issue, we first need to know how to prevent text selection in the PDF. I hope that there’s a flag in the PDF text object for this, or we’ll have to find another solution.

If someone wants to take the time to find relevant information in the PDF specification, that would be really helpful! Then we’ll be able to write the code needed to implement the feature in WeasyPrint, and possibly in pydyf.

liZe avatar Sep 24 '24 14:09 liZe

Unfortunately, it doesn't look as if there's a built-in function in the PDF spec for this. I'm guessing a "workaround" could be to simply print user-select: none text as vector paths instead of text objects. Well, I say "simply", but this probably requires quite a bit of effort to achieve? I'm not familiar with the internals of WeasyPrint/pydyf.

nnsee avatar Sep 24 '24 14:09 nnsee

Well, I say "simply", but this probably requires quite a bit of effort to achieve?

We currently let the PDF reader draw the text for us, so it may not be that "simple" to add the feature :smile:.

You can try to set the text rendering mode (chapter 9.3.6 of the PDF 2.0 spec) to 7, and see if it can be selected in various PDF readers. If it can’t, then we may have found a solution.

(This page and this function can be useful.)

liZe avatar Sep 24 '24 15:09 liZe

Unfortunately, text rendering mode 7 simply doesn't render the text (but the text is still selectable). I went through all of the modes and none of them prevent the text from being selected. It looks like this is quite a difficult problem to solve.

nnsee avatar Sep 25 '24 08:09 nnsee

If anyone stumbles upon this issue and would like to draw text glyphs directly without the text being selectable, you can adapt this example code to your needs:

import pydyf
from fontTools.ttLib import TTFont
from fontTools.pens.basePen import BasePen
from fontTools.pens.transformPen import TransformPen

class PDFPen(BasePen):
    def __init__(self, glyphSet, stream):
        super().__init__(glyphSet)
        self.stream = stream

    def _moveTo(self, pt):
        x, y = pt
        self.stream.move_to(x, y)

    def _lineTo(self, pt):
        x, y = pt
        self.stream.line_to(x, y)

    def _curveToOne(self, pt1, pt2, pt3):
        self.stream.curve_to(pt1[0], pt1[1], pt2[0], pt2[1], pt3[0], pt3[1])

    def _closePath(self):
        self.stream.close()

def draw_glyphs_as_paths(text, ttf, glyph_set, stream, x=0, y=0, font_size=12):
    hmtx = ttf['hmtx']
    cmap = ttf.getBestCmap()
    units_per_em = ttf['head'].unitsPerEm
    scale = font_size / units_per_em

    tx, ty = x, y

    for char in text:
        glyph_index = ord(char)
        glyph_name = cmap.get(glyph_index)
        if not glyph_name:
            continue

        glyph = glyph_set[glyph_name]

        pen = PDFPen(glyph_set, stream)
        transform = (scale, 0, 0, scale, tx, ty)
        tp = TransformPen(pen, transform)
        glyph.draw(tp)

        stream.fill()

        advance_width, _ = hmtx[glyph_name]
        tx += advance_width * scale

ttf = TTFont('/usr/share/fonts/noto/NotoSans-Regular.ttf')  # replace with real path to font
glyph_set = ttf.getGlyphSet()

document = pydyf.PDF()
stream = pydyf.Stream()
stream.push_state()

draw_glyphs_as_paths('Bœuf grillé & café', ttf, glyph_set, stream, x=10, y=90, font_size=20)

stream.pop_state()
document.add_object(stream)

document.add_page(pydyf.Dictionary({
    'Type': '/Page',
    'Parent': document.pages.reference,
    'MediaBox': [0, 0, 200, 200],
    'Contents': stream.reference,
    'Resources': {
        'ProcSet': ['/PDF', '/Text'],
    },
}))

with open('document.pdf', 'wb') as f:
    document.write(f)

nnsee avatar Sep 25 '24 09:09 nnsee

Thanks for the investigation, and thanks for sharing the code. It should work for "simple" cases, but I think that it would be difficult to include in WeasyPrint to work in all cases.

I have the feeling that a dirty/smart workaround exists and just has to be found … but that’s just an intuition.

liZe avatar Sep 25 '24 20:09 liZe