Repository: xhtml2pdf/xhtml2pdf

Issue:
Unable to render Base64 CSS Font.
I am trying to embed base64 font in my HTML, but the library can load it. I made some research and found that in file util.py:519 there is a condition that checks if the uri starts with "data:", but the passed uri is a list, that is evaluated to str, so the string is "[u'data:..'" and the if can match it. I tried couple of tricky things, but after the match another errors came out. So, can you help me guys to fix this issue? Thanks a lot!

```
# Data URI
        if uri.startswith("data:"): # uri="[u'data:....']"
            m = _rx_datauri.match(uri)
            self.mimetype = m.group("mime")
            self.data = base64.decodestring(m.group("data"))
```

That is how my html file looks like:

```
@font-face {
                font-family: "Trebuchet MS";
                font-style: normal;
                font-weight: normal;
                src: url(data:font/truetype;base64,AAEA......) format('truetype');
}
```


Discussion:

Patch:
diff --git a/xhtml2pdf/context.py b/xhtml2pdf/context.py
index deded4c..240246b 100644
--- a/xhtml2pdf/context.py
+++ b/xhtml2pdf/context.py
@@ -37,7 +37,7 @@ except ImportError:
 from reportlab.platypus.paraparser import ParaFrag, ps2tt, tt2ps
 
 from xhtml2pdf import default, parser
-from xhtml2pdf.files import getFile, pisaFileObject
+from xhtml2pdf.files import B64InlineURI, getFile, pisaFileObject
 from xhtml2pdf.tables import TableData
 from xhtml2pdf.util import (
     arabic_format,
@@ -1084,13 +1084,22 @@ class pisaContext:
 
             # XXX Problems with unicode here
             fontAlias = [str(x) for x in fontAlias]
-
             fontName = fontAlias[0]
-            parts = src.split(".")
-            baseName, suffix = ".".join(parts[:-1]), parts[-1]
-            suffix = suffix.lower()
 
-            if suffix in {"ttc", "ttf"}:
+            font_type = None
+            if isinstance(file.instance, B64InlineURI):
+                if file.getMimeType() == "font/ttf":
+                    font_type = "ttf"
+            else:
+                parts = src.split(".")
+                baseName, suffix = ".".join(parts[:-1]), parts[-1]
+                suffix = suffix.lower()
+                if suffix in ("ttf", "ttc"):
+                    font_type = "ttf"
+                elif suffix in ("afm", "pfb"):
+                    font_type = suffix
+
+            if font_type == "ttf":
                 # determine full font name according to weight and style
                 fullFontName = "%s_%d%d" % (fontName, bold, italic)
 
@@ -1118,8 +1127,8 @@ class pisaContext:
                     # Register "normal" name and the place holder for style
                     self.registerFont(fontName, [*fontAlias, fullFontName])
 
-            elif suffix in {"afm", "pfb"}:
-                if suffix == "afm":
+            elif font_type in ("afm", ""):
+                if font_type == "afm":
                     afm = file.getNamedFile()
                     tfile = pisaFileObject(baseName + ".pfb", basepath=file.basepath)
                     pfb = tfile.getNamedFile()
diff --git a/xhtml2pdf/files.py b/xhtml2pdf/files.py
index c891733..c2b0ea0 100644
--- a/xhtml2pdf/files.py
+++ b/xhtml2pdf/files.py
@@ -5,7 +5,6 @@ import gzip
 import http.client as httplib
 import logging
 import mimetypes
-import re
 import sys
 import tempfile
 import threading
@@ -199,20 +198,25 @@ class BaseFile:
 
 
 class B64InlineURI(BaseFile):
-    _rx_datauri = re.compile(
-        "^data:(?P<mime>[a-z]+/[a-z]+);base64,(?P<data>.*)$", re.M | re.DOTALL
-    )
+    mime_params: list
 
     def extract_data(self) -> bytes | None:
-        m = self._rx_datauri.match(self.path)
-        if not m:
-            msg = "Inline data could not be parsed"
+        # RFC 2397 form: data:[<mediatype>][;base64],<data>
+        parts = self.path.split("base64,")
+        if (
+            not self.path.startswith("data:")
+            or "base64," not in self.path
+            or len(parts) != 2
+        ):
+            msg = "Base64-encoded data URI is mailformed"
             raise RuntimeError(msg)
-        self.mimetype = m.group("mime")
-
-        # Support URL encoded strings
-        b64: bytes = urllib_unquote(m.group("data")).encode("utf-8")
+        data = parts[1]
+        # Strip 'data:' prefix and split mime type with optional params
+        mime = parts[0][len("data:") :].split(";")
+        # mime_params are preserved for future use
+        self.mimetype, self.mime_params = mime[0], mime[1:]
 
+        b64: bytes = urllib_unquote(data).encode("utf-8")
         return base64.b64decode(b64)
 
 


Test patch:
diff --git a/tests/test_parser.py b/tests/test_parser.py
index bc4f81b..3d885fd 100644
--- a/tests/test_parser.py
+++ b/tests/test_parser.py
@@ -1,3 +1,4 @@
+import base64
 import os
 from unittest import TestCase
 
@@ -58,6 +59,15 @@ class ParserTest(TestCase):
         r = pisaParser(data, c)
         self.assertEqual(r.warn, 0)
 
+    def test_image_base64_params(self) -> None:
+        c = pisaContext(".")
+        data = (
+            b"<img"
+            b' src="data:image/gif;foo=bar;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=">'
+        )
+        r = pisaParser(data, c)
+        self.assertEqual(r.warn, 0)
+
     def test_image_base64_urlencoded(self) -> None:
         c = pisaContext(".")
         data = (
@@ -66,3 +76,27 @@ class ParserTest(TestCase):
         )
         r = pisaParser(data, c)
         self.assertEqual(r.warn, 0)
+
+    def test_font_base64(self) -> None:
+        ttf_path = os.path.join(
+            os.path.dirname(os.path.realpath(__file__)),
+            "samples",
+            "font",
+            "Noto_Sans",
+            "NotoSans-Regular.ttf",
+        )
+        with open(ttf_path, "rb") as f:
+            b64_font = base64.b64encode(f.read()).decode("ascii")
+        c = pisaContext(".")
+
+        data = b"""
+          <style>
+            @font-face {
+              font-family: 'FontName';
+              src: url('data:font/ttf;charset=utf-8;base64,%s');
+            }
+          </style>
+        """ % b64_font.encode("utf-8")
+
+        r = pisaParser(data, c)
+        self.assertEqual(r.warn, 0)