Preventing text selection does not work with user-select: none
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!
Hello! Has there been any progress at all with this feature? Are there any blockers that need solving before this can be implemented?
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.
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.
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.)
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.
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)
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.