|  | @ -4,7 +4,9 @@ set -e | ||||||
| 
 | 
 | ||||||
| sudo apt-get update | sudo apt-get update | ||||||
| sudo apt-get -qq install libfreetype6-dev liblcms2-dev python-tk\ | sudo apt-get -qq install libfreetype6-dev liblcms2-dev python-tk\ | ||||||
| 			 python-qt4 ghostscript libffi-dev libjpeg-turbo-progs cmake imagemagick | 			 python-qt4 ghostscript libffi-dev libjpeg-turbo-progs cmake imagemagick\ | ||||||
|  |              libharfbuzz-dev libfribidi-dev | ||||||
|  | 
 | ||||||
| pip install cffi | pip install cffi | ||||||
| pip install nose | pip install nose | ||||||
| pip install check-manifest | pip install check-manifest | ||||||
|  |  | ||||||
|  | @ -207,7 +207,6 @@ class ImageDraw(object): | ||||||
|         if self._multiline_check(text): |         if self._multiline_check(text): | ||||||
|             return self.multiline_text(xy, text, fill, font, anchor, |             return self.multiline_text(xy, text, fill, font, anchor, | ||||||
|                                        *args, **kwargs) |                                        *args, **kwargs) | ||||||
| 
 |  | ||||||
|         ink, fill = self._getink(fill) |         ink, fill = self._getink(fill) | ||||||
|         if font is None: |         if font is None: | ||||||
|             font = self.getfont() |             font = self.getfont() | ||||||
|  | @ -215,17 +214,17 @@ class ImageDraw(object): | ||||||
|             ink = fill |             ink = fill | ||||||
|         if ink is not None: |         if ink is not None: | ||||||
|             try: |             try: | ||||||
|                 mask, offset = font.getmask2(text, self.fontmode) |                 mask, offset = font.getmask2(text, self.fontmode, *args, **kwargs) | ||||||
|                 xy = xy[0] + offset[0], xy[1] + offset[1] |                 xy = xy[0] + offset[0], xy[1] + offset[1] | ||||||
|             except AttributeError: |             except AttributeError: | ||||||
|                 try: |                 try: | ||||||
|                     mask = font.getmask(text, self.fontmode) |                     mask = font.getmask(text, self.fontmode, *args, **kwargs) | ||||||
|                 except TypeError: |                 except TypeError: | ||||||
|                     mask = font.getmask(text) |                     mask = font.getmask(text) | ||||||
|             self.draw.draw_bitmap(xy, mask, ink) |             self.draw.draw_bitmap(xy, mask, ink) | ||||||
| 
 | 
 | ||||||
|     def multiline_text(self, xy, text, fill=None, font=None, anchor=None, |     def multiline_text(self, xy, text, fill=None, font=None, anchor=None, | ||||||
|                        spacing=4, align="left"): |                        spacing=4, align="left", direction=None, features=None): | ||||||
|         widths = [] |         widths = [] | ||||||
|         max_width = 0 |         max_width = 0 | ||||||
|         lines = self._multiline_split(text) |         lines = self._multiline_split(text) | ||||||
|  | @ -244,25 +243,30 @@ class ImageDraw(object): | ||||||
|                 left += (max_width - widths[idx]) |                 left += (max_width - widths[idx]) | ||||||
|             else: |             else: | ||||||
|                 assert False, 'align must be "left", "center" or "right"' |                 assert False, 'align must be "left", "center" or "right"' | ||||||
|             self.text((left, top), line, fill, font, anchor) |             self.text((left, top), line, fill, font, anchor, | ||||||
|  |                       direction=direction, features=features) | ||||||
|             top += line_spacing |             top += line_spacing | ||||||
|             left = xy[0] |             left = xy[0] | ||||||
| 
 | 
 | ||||||
|     def textsize(self, text, font=None, *args, **kwargs): |     def textsize(self, text, font=None, spacing=4, direction=None, | ||||||
|  |                  features=None): | ||||||
|         """Get the size of a given string, in pixels.""" |         """Get the size of a given string, in pixels.""" | ||||||
|         if self._multiline_check(text): |         if self._multiline_check(text): | ||||||
|             return self.multiline_textsize(text, font, *args, **kwargs) |             return self.multiline_textsize(text, font, spacing, | ||||||
|  |                                            direction, features) | ||||||
| 
 | 
 | ||||||
|         if font is None: |         if font is None: | ||||||
|             font = self.getfont() |             font = self.getfont() | ||||||
|         return font.getsize(text) |         return font.getsize(text, direction, features) | ||||||
| 
 | 
 | ||||||
|     def multiline_textsize(self, text, font=None, spacing=4): |     def multiline_textsize(self, text, font=None, spacing=4, direction=None, | ||||||
|  |                            features=None): | ||||||
|         max_width = 0 |         max_width = 0 | ||||||
|         lines = self._multiline_split(text) |         lines = self._multiline_split(text) | ||||||
|         line_spacing = self.textsize('A', font=font)[1] + spacing |         line_spacing = self.textsize('A', font=font)[1] + spacing | ||||||
|         for line in lines: |         for line in lines: | ||||||
|             line_width, line_height = self.textsize(line, font) |             line_width, line_height = self.textsize(line, font, spacing, | ||||||
|  |                                                     direction, features) | ||||||
|             max_width = max(max_width, line_width) |             max_width = max(max_width, line_width) | ||||||
|         return max_width, len(lines)*line_spacing |         return max_width, len(lines)*line_spacing | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -41,6 +41,9 @@ try: | ||||||
| except ImportError: | except ImportError: | ||||||
|     core = _imagingft_not_installed() |     core = _imagingft_not_installed() | ||||||
| 
 | 
 | ||||||
|  | LAYOUT_BASIC = 0 | ||||||
|  | LAYOUT_RAQM = 1 | ||||||
|  | 
 | ||||||
| # FIXME: add support for pilfont2 format (see FontFile.py) | # FIXME: add support for pilfont2 format (see FontFile.py) | ||||||
| 
 | 
 | ||||||
| # -------------------------------------------------------------------- | # -------------------------------------------------------------------- | ||||||
|  | @ -103,9 +106,12 @@ class ImageFont(object): | ||||||
| 
 | 
 | ||||||
|         self.font = Image.core.font(image.im, data) |         self.font = Image.core.font(image.im, data) | ||||||
| 
 | 
 | ||||||
|         # delegate critical operations to internal type |     def getsize(self, text, *args, **kwargs): | ||||||
|         self.getsize = self.font.getsize |         return self.font.getsize(text) | ||||||
|         self.getmask = self.font.getmask | 
 | ||||||
|  |     def getmask(self, text, mode="", *args, **kwargs): | ||||||
|  |         return self.font.getmask(text, mode) | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| ## | ## | ||||||
|  | @ -115,7 +121,8 @@ class ImageFont(object): | ||||||
| class FreeTypeFont(object): | class FreeTypeFont(object): | ||||||
|     "FreeType font wrapper (requires _imagingft service)" |     "FreeType font wrapper (requires _imagingft service)" | ||||||
| 
 | 
 | ||||||
|     def __init__(self, font=None, size=10, index=0, encoding=""): |     def __init__(self, font=None, size=10, index=0, encoding="", | ||||||
|  |                  layout_engine=None): | ||||||
|         # FIXME: use service provider instead |         # FIXME: use service provider instead | ||||||
| 
 | 
 | ||||||
|         self.path = font |         self.path = font | ||||||
|  | @ -123,12 +130,21 @@ class FreeTypeFont(object): | ||||||
|         self.index = index |         self.index = index | ||||||
|         self.encoding = encoding |         self.encoding = encoding | ||||||
| 
 | 
 | ||||||
|  |         if layout_engine not in (LAYOUT_BASIC, LAYOUT_RAQM): | ||||||
|  |             layout_engine = LAYOUT_BASIC | ||||||
|  |             if core.HAVE_RAQM: | ||||||
|  |                 layout_engine = LAYOUT_RAQM | ||||||
|  |         if layout_engine == LAYOUT_RAQM and not core.HAVE_RAQM: | ||||||
|  |             layout_engine = LAYOUT_BASIC | ||||||
|  | 
 | ||||||
|  |         self.layout_engine = layout_engine | ||||||
|  | 
 | ||||||
|         if isPath(font): |         if isPath(font): | ||||||
|             self.font = core.getfont(font, size, index, encoding) |             self.font = core.getfont(font, size, index, encoding, layout_engine=layout_engine) | ||||||
|         else: |         else: | ||||||
|             self.font_bytes = font.read() |             self.font_bytes = font.read() | ||||||
|             self.font = core.getfont( |             self.font = core.getfont( | ||||||
|                 "", size, index, encoding, self.font_bytes) |                 "", size, index, encoding, self.font_bytes, layout_engine) | ||||||
| 
 | 
 | ||||||
|     def getname(self): |     def getname(self): | ||||||
|         return self.font.family, self.font.style |         return self.font.family, self.font.style | ||||||
|  | @ -136,23 +152,24 @@ class FreeTypeFont(object): | ||||||
|     def getmetrics(self): |     def getmetrics(self): | ||||||
|         return self.font.ascent, self.font.descent |         return self.font.ascent, self.font.descent | ||||||
| 
 | 
 | ||||||
|     def getsize(self, text): |     def getsize(self, text, direction=None, features=None): | ||||||
|         size, offset = self.font.getsize(text) |         size, offset = self.font.getsize(text, direction, features) | ||||||
|         return (size[0] + offset[0], size[1] + offset[1]) |         return (size[0] + offset[0], size[1] + offset[1]) | ||||||
| 
 | 
 | ||||||
|     def getoffset(self, text): |     def getoffset(self, text): | ||||||
|         return self.font.getsize(text)[1] |         return self.font.getsize(text)[1] | ||||||
| 
 | 
 | ||||||
|     def getmask(self, text, mode=""): |     def getmask(self, text, mode="", direction=None, features=None): | ||||||
|         return self.getmask2(text, mode)[0] |         return self.getmask2(text, mode, direction=direction, features=features)[0] | ||||||
| 
 | 
 | ||||||
|     def getmask2(self, text, mode="", fill=Image.core.fill): |     def getmask2(self, text, mode="", fill=Image.core.fill, direction=None, features=None): | ||||||
|         size, offset = self.font.getsize(text) |         size, offset = self.font.getsize(text, direction, features) | ||||||
|         im = fill("L", size, 0) |         im = fill("L", size, 0) | ||||||
|         self.font.render(text, im.id, mode == "1") |         self.font.render(text, im.id, mode == "1", direction, features) | ||||||
|         return im, offset |         return im, offset | ||||||
| 
 | 
 | ||||||
|     def font_variant(self, font=None, size=None, index=None, encoding=None): |     def font_variant(self, font=None, size=None, index=None, encoding=None, | ||||||
|  |                      layout_engine=None): | ||||||
|         """ |         """ | ||||||
|         Create a copy of this FreeTypeFont object, |         Create a copy of this FreeTypeFont object, | ||||||
|         using any specified arguments to override the settings. |         using any specified arguments to override the settings. | ||||||
|  | @ -165,8 +182,9 @@ class FreeTypeFont(object): | ||||||
|         return FreeTypeFont(font=self.path if font is None else font, |         return FreeTypeFont(font=self.path if font is None else font, | ||||||
|                             size=self.size if size is None else size, |                             size=self.size if size is None else size, | ||||||
|                             index=self.index if index is None else index, |                             index=self.index if index is None else index, | ||||||
|                             encoding=self.encoding if encoding is None else |                             encoding=self.encoding if encoding is None else encoding, | ||||||
|                             encoding) |                             layout_engine=self.layout_engine if layout_engine is None else layout_engine | ||||||
|  |                             ) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class TransposedFont(object): | class TransposedFont(object): | ||||||
|  | @ -185,14 +203,14 @@ class TransposedFont(object): | ||||||
|         self.font = font |         self.font = font | ||||||
|         self.orientation = orientation  # any 'transpose' argument, or None |         self.orientation = orientation  # any 'transpose' argument, or None | ||||||
| 
 | 
 | ||||||
|     def getsize(self, text): |     def getsize(self, text, *args, **kwargs): | ||||||
|         w, h = self.font.getsize(text) |         w, h = self.font.getsize(text) | ||||||
|         if self.orientation in (Image.ROTATE_90, Image.ROTATE_270): |         if self.orientation in (Image.ROTATE_90, Image.ROTATE_270): | ||||||
|             return h, w |             return h, w | ||||||
|         return w, h |         return w, h | ||||||
| 
 | 
 | ||||||
|     def getmask(self, text, mode=""): |     def getmask(self, text, mode="", *args, **kwargs): | ||||||
|         im = self.font.getmask(text, mode) |         im = self.font.getmask(text, mode, *args, **kwargs) | ||||||
|         if self.orientation is not None: |         if self.orientation is not None: | ||||||
|             return im.transpose(self.orientation) |             return im.transpose(self.orientation) | ||||||
|         return im |         return im | ||||||
|  | @ -212,7 +230,8 @@ def load(filename): | ||||||
|     return f |     return f | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def truetype(font=None, size=10, index=0, encoding=""): | def truetype(font=None, size=10, index=0, encoding="", | ||||||
|  |              layout_engine=None): | ||||||
|     """ |     """ | ||||||
|     Load a TrueType or OpenType font file, and create a font object. |     Load a TrueType or OpenType font file, and create a font object. | ||||||
|     This function loads a font object from the given file, and creates |     This function loads a font object from the given file, and creates | ||||||
|  | @ -230,12 +249,14 @@ def truetype(font=None, size=10, index=0, encoding=""): | ||||||
|                      Symbol), "ADOB" (Adobe Standard), "ADBE" (Adobe Expert), |                      Symbol), "ADOB" (Adobe Standard), "ADBE" (Adobe Expert), | ||||||
|                      and "armn" (Apple Roman). See the FreeType documentation |                      and "armn" (Apple Roman). See the FreeType documentation | ||||||
|                      for more information. |                      for more information. | ||||||
|  |     :param layout_engine: Which layout engine to use, if available: | ||||||
|  |                      `ImageFont.LAYOUT_BASIC` or `ImageFont.LAYOUT_RAQM`.                   | ||||||
|     :return: A font object. |     :return: A font object. | ||||||
|     :exception IOError: If the file could not be read. |     :exception IOError: If the file could not be read. | ||||||
|     """ |     """ | ||||||
| 
 | 
 | ||||||
|     try: |     try: | ||||||
|         return FreeTypeFont(font, size, index, encoding) |         return FreeTypeFont(font, size, index, encoding, layout_engine) | ||||||
|     except IOError: |     except IOError: | ||||||
|         ttf_filename = os.path.basename(font) |         ttf_filename = os.path.basename(font) | ||||||
| 
 | 
 | ||||||
|  | @ -266,16 +287,16 @@ def truetype(font=None, size=10, index=0, encoding=""): | ||||||
|                 for walkfilename in walkfilenames: |                 for walkfilename in walkfilenames: | ||||||
|                     if ext and walkfilename == ttf_filename: |                     if ext and walkfilename == ttf_filename: | ||||||
|                         fontpath = os.path.join(walkroot, walkfilename) |                         fontpath = os.path.join(walkroot, walkfilename) | ||||||
|                         return FreeTypeFont(fontpath, size, index, encoding) |                         return FreeTypeFont(fontpath, size, index, encoding, layout_engine) | ||||||
|                     elif not ext and os.path.splitext(walkfilename)[0] == ttf_filename: |                     elif not ext and os.path.splitext(walkfilename)[0] == ttf_filename: | ||||||
|                         fontpath = os.path.join(walkroot, walkfilename) |                         fontpath = os.path.join(walkroot, walkfilename) | ||||||
|                         if os.path.splitext(fontpath)[1] == '.ttf': |                         if os.path.splitext(fontpath)[1] == '.ttf': | ||||||
|                             return FreeTypeFont(fontpath, size, index, encoding) |                             return FreeTypeFont(fontpath, size, index, encoding, layout_engine) | ||||||
|                         if not ext and first_font_with_a_different_extension is None: |                         if not ext and first_font_with_a_different_extension is None: | ||||||
|                             first_font_with_a_different_extension = fontpath |                             first_font_with_a_different_extension = fontpath | ||||||
|         if first_font_with_a_different_extension: |         if first_font_with_a_different_extension: | ||||||
|             return FreeTypeFont(first_font_with_a_different_extension, size, |             return FreeTypeFont(first_font_with_a_different_extension, size, | ||||||
|                                 index, encoding) |                                 index, encoding, layout_engine) | ||||||
|         raise |         raise | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -45,6 +45,7 @@ def get_supported_codecs(): | ||||||
| features = { | features = { | ||||||
|     "webp_mux": ("PIL._webp", 'HAVE_WEBPMUX'), |     "webp_mux": ("PIL._webp", 'HAVE_WEBPMUX'), | ||||||
|     "transp_webp": ("PIL._webp", "HAVE_TRANSPARENCY"), |     "transp_webp": ("PIL._webp", "HAVE_TRANSPARENCY"), | ||||||
|  |     "raqm": ("PIL._imagingft", "HAVE_RAQM") | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| def check_feature(feature): | def check_feature(feature): | ||||||
|  |  | ||||||
							
								
								
									
										6
									
								
								Tests/fonts/LICENSE.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						|  | @ -0,0 +1,6 @@ | ||||||
|  | 
 | ||||||
|  | NotoNastaliqUrdu-Regular.ttf: | ||||||
|  | 
 | ||||||
|  | (from https://github.com/googlei18n/noto-fonts) | ||||||
|  | 
 | ||||||
|  | All Noto fonts are published under the SIL Open Font License (OFL) v1.1 (http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL), which allows you to copy, modify, and redistribute them if you need to. | ||||||
							
								
								
									
										
											BIN
										
									
								
								Tests/fonts/NotoNastaliqUrdu-Regular.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								Tests/images/test_Nastalifont_text.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.0 KiB | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/test_arabictext_features.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.4 KiB | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/test_complex_unicode_text.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.4 KiB | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/test_direction_ltr.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.8 KiB | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/test_direction_rtl.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.2 KiB | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/test_kerning_features.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 963 B | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/test_ligature_features.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 605 B | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/test_text.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.1 KiB | 
							
								
								
									
										
											BIN
										
									
								
								Tests/images/test_y_offset.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.6 KiB | 
|  | @ -1,7 +1,6 @@ | ||||||
| from helper import unittest, PillowTestCase | from helper import unittest, PillowTestCase | ||||||
| 
 | 
 | ||||||
| from PIL import Image | from PIL import Image, ImageDraw, ImageFont, features | ||||||
| from PIL import ImageDraw |  | ||||||
| from io import BytesIO | from io import BytesIO | ||||||
| import os | import os | ||||||
| import sys | import sys | ||||||
|  | @ -12,10 +11,9 @@ FONT_SIZE = 20 | ||||||
| 
 | 
 | ||||||
| TEST_TEXT = "hey you\nyou are awesome\nthis looks awkward" | TEST_TEXT = "hey you\nyou are awesome\nthis looks awkward" | ||||||
| 
 | 
 | ||||||
|  | HAS_FREETYPE = features.check('freetype2') | ||||||
|  | HAS_RAQM = features.check('raqm') | ||||||
| 
 | 
 | ||||||
| try: |  | ||||||
|     from PIL import ImageFont |  | ||||||
|     ImageFont.core.getfont  # check if freetype is available |  | ||||||
| 
 | 
 | ||||||
| class SimplePatcher(object): | class SimplePatcher(object): | ||||||
|     def __init__(self, parent_obj, attr_name, value): |     def __init__(self, parent_obj, attr_name, value): | ||||||
|  | @ -42,14 +40,20 @@ try: | ||||||
|         else: |         else: | ||||||
|             delattr(self._parent_obj, self._attr_name) |             delattr(self._parent_obj, self._attr_name) | ||||||
| 
 | 
 | ||||||
|  | @unittest.skipUnless(HAS_FREETYPE, "ImageFont not Available") | ||||||
| class TestImageFont(PillowTestCase): | class TestImageFont(PillowTestCase): | ||||||
|  |     LAYOUT_ENGINE = ImageFont.LAYOUT_BASIC | ||||||
|  | 
 | ||||||
|  |     def get_font(self): | ||||||
|  |         return ImageFont.truetype(FONT_PATH, FONT_SIZE, | ||||||
|  |                                   layout_engine=self.LAYOUT_ENGINE) | ||||||
|      |      | ||||||
|     def test_sanity(self): |     def test_sanity(self): | ||||||
|         self.assertRegexpMatches( |         self.assertRegexpMatches( | ||||||
|             ImageFont.core.freetype2_version, r"\d+\.\d+\.\d+$") |             ImageFont.core.freetype2_version, r"\d+\.\d+\.\d+$") | ||||||
| 
 | 
 | ||||||
|     def test_font_properties(self): |     def test_font_properties(self): | ||||||
|             ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) |         ttf = self.get_font() | ||||||
|         self.assertEqual(ttf.path, FONT_PATH) |         self.assertEqual(ttf.path, FONT_PATH) | ||||||
|         self.assertEqual(ttf.size, FONT_SIZE) |         self.assertEqual(ttf.size, FONT_SIZE) | ||||||
| 
 | 
 | ||||||
|  | @ -65,9 +69,8 @@ try: | ||||||
|         self.assertEqual(ttf_copy.path, second_font_path) |         self.assertEqual(ttf_copy.path, second_font_path) | ||||||
| 
 | 
 | ||||||
|     def test_font_with_name(self): |     def test_font_with_name(self): | ||||||
|             ImageFont.truetype(FONT_PATH, FONT_SIZE) |         self.get_font() | ||||||
|         self._render(FONT_PATH) |         self._render(FONT_PATH) | ||||||
|             self._clean() |  | ||||||
| 
 | 
 | ||||||
|     def _font_as_bytes(self): |     def _font_as_bytes(self): | ||||||
|         with open(FONT_PATH, 'rb') as f: |         with open(FONT_PATH, 'rb') as f: | ||||||
|  | @ -75,34 +78,30 @@ try: | ||||||
|         return font_bytes |         return font_bytes | ||||||
| 
 | 
 | ||||||
|     def test_font_with_filelike(self): |     def test_font_with_filelike(self): | ||||||
|             ImageFont.truetype(self._font_as_bytes(), FONT_SIZE) |         ImageFont.truetype(self._font_as_bytes(), FONT_SIZE, | ||||||
|  |                            layout_engine=self.LAYOUT_ENGINE) | ||||||
|         self._render(self._font_as_bytes()) |         self._render(self._font_as_bytes()) | ||||||
|         # Usage note:  making two fonts from the same buffer fails. |         # Usage note:  making two fonts from the same buffer fails. | ||||||
|         # shared_bytes = self._font_as_bytes() |         # shared_bytes = self._font_as_bytes() | ||||||
|         # self._render(shared_bytes) |         # self._render(shared_bytes) | ||||||
|         # self.assertRaises(Exception, lambda: _render(shared_bytes)) |         # self.assertRaises(Exception, lambda: _render(shared_bytes)) | ||||||
|             self._clean() |  | ||||||
| 
 | 
 | ||||||
|     def test_font_with_open_file(self): |     def test_font_with_open_file(self): | ||||||
|         with open(FONT_PATH, 'rb') as f: |         with open(FONT_PATH, 'rb') as f: | ||||||
|             self._render(f) |             self._render(f) | ||||||
|             self._clean() |  | ||||||
| 
 | 
 | ||||||
|     def _render(self, font): |     def _render(self, font): | ||||||
|         txt = "Hello World!" |         txt = "Hello World!" | ||||||
|             ttf = ImageFont.truetype(font, FONT_SIZE) |         ttf = ImageFont.truetype(font, FONT_SIZE, | ||||||
|  |                                  layout_engine=self.LAYOUT_ENGINE) | ||||||
|         ttf.getsize(txt) |         ttf.getsize(txt) | ||||||
| 
 | 
 | ||||||
|         img = Image.new("RGB", (256, 64), "white") |         img = Image.new("RGB", (256, 64), "white") | ||||||
|         d = ImageDraw.Draw(img) |         d = ImageDraw.Draw(img) | ||||||
|         d.text((10, 10), txt, font=ttf, fill='black') |         d.text((10, 10), txt, font=ttf, fill='black') | ||||||
| 
 | 
 | ||||||
|             img.save('font.png') |  | ||||||
|         return img |         return img | ||||||
| 
 | 
 | ||||||
|         def _clean(self): |  | ||||||
|             os.unlink('font.png') |  | ||||||
| 
 |  | ||||||
|     def test_render_equal(self): |     def test_render_equal(self): | ||||||
|         img_path = self._render(FONT_PATH) |         img_path = self._render(FONT_PATH) | ||||||
|         with open(FONT_PATH, 'rb') as f: |         with open(FONT_PATH, 'rb') as f: | ||||||
|  | @ -110,12 +109,11 @@ try: | ||||||
|         img_filelike = self._render(font_filelike) |         img_filelike = self._render(font_filelike) | ||||||
| 
 | 
 | ||||||
|         self.assert_image_equal(img_path, img_filelike) |         self.assert_image_equal(img_path, img_filelike) | ||||||
|             self._clean() |  | ||||||
| 
 | 
 | ||||||
|     def test_textsize_equal(self): |     def test_textsize_equal(self): | ||||||
|         im = Image.new(mode='RGB', size=(300, 100)) |         im = Image.new(mode='RGB', size=(300, 100)) | ||||||
|         draw = ImageDraw.Draw(im) |         draw = ImageDraw.Draw(im) | ||||||
|             ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) |         ttf = self.get_font() | ||||||
| 
 | 
 | ||||||
|         txt = "Hello World!" |         txt = "Hello World!" | ||||||
|         size = draw.textsize(txt, ttf) |         size = draw.textsize(txt, ttf) | ||||||
|  | @ -132,7 +130,7 @@ try: | ||||||
|     def test_render_multiline(self): |     def test_render_multiline(self): | ||||||
|         im = Image.new(mode='RGB', size=(300, 100)) |         im = Image.new(mode='RGB', size=(300, 100)) | ||||||
|         draw = ImageDraw.Draw(im) |         draw = ImageDraw.Draw(im) | ||||||
|             ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) |         ttf = self.get_font() | ||||||
|         line_spacing = draw.textsize('A', font=ttf)[1] + 4 |         line_spacing = draw.textsize('A', font=ttf)[1] + 4 | ||||||
|         lines = TEST_TEXT.split("\n") |         lines = TEST_TEXT.split("\n") | ||||||
|         y = 0 |         y = 0 | ||||||
|  | @ -149,7 +147,7 @@ try: | ||||||
|         self.assert_image_similar(im, target_img, 6.2) |         self.assert_image_similar(im, target_img, 6.2) | ||||||
| 
 | 
 | ||||||
|     def test_render_multiline_text(self): |     def test_render_multiline_text(self): | ||||||
|             ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) |         ttf = self.get_font() | ||||||
| 
 | 
 | ||||||
|         # Test that text() correctly connects to multiline_text() |         # Test that text() correctly connects to multiline_text() | ||||||
|         # and that align defaults to left |         # and that align defaults to left | ||||||
|  | @ -187,7 +185,7 @@ try: | ||||||
|     def test_unknown_align(self): |     def test_unknown_align(self): | ||||||
|         im = Image.new(mode='RGB', size=(300, 100)) |         im = Image.new(mode='RGB', size=(300, 100)) | ||||||
|         draw = ImageDraw.Draw(im) |         draw = ImageDraw.Draw(im) | ||||||
|             ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) |         ttf = self.get_font() | ||||||
| 
 | 
 | ||||||
|         # Act/Assert |         # Act/Assert | ||||||
|         self.assertRaises(AssertionError, |         self.assertRaises(AssertionError, | ||||||
|  | @ -196,7 +194,7 @@ try: | ||||||
|                                                       align="unknown")) |                                                       align="unknown")) | ||||||
| 
 | 
 | ||||||
|     def test_multiline_size(self): |     def test_multiline_size(self): | ||||||
|             ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) |         ttf = self.get_font() | ||||||
|         im = Image.new(mode='RGB', size=(300, 100)) |         im = Image.new(mode='RGB', size=(300, 100)) | ||||||
|         draw = ImageDraw.Draw(im) |         draw = ImageDraw.Draw(im) | ||||||
| 
 | 
 | ||||||
|  | @ -211,7 +209,7 @@ try: | ||||||
|         del draw |         del draw | ||||||
| 
 | 
 | ||||||
|     def test_multiline_width(self): |     def test_multiline_width(self): | ||||||
|             ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) |         ttf = self.get_font() | ||||||
|         im = Image.new(mode='RGB', size=(300, 100)) |         im = Image.new(mode='RGB', size=(300, 100)) | ||||||
|         draw = ImageDraw.Draw(im) |         draw = ImageDraw.Draw(im) | ||||||
| 
 | 
 | ||||||
|  | @ -221,7 +219,7 @@ try: | ||||||
|         del draw |         del draw | ||||||
| 
 | 
 | ||||||
|     def test_multiline_spacing(self): |     def test_multiline_spacing(self): | ||||||
|             ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) |         ttf = self.get_font() | ||||||
| 
 | 
 | ||||||
|         im = Image.new(mode='RGB', size=(300, 100)) |         im = Image.new(mode='RGB', size=(300, 100)) | ||||||
|         draw = ImageDraw.Draw(im) |         draw = ImageDraw.Draw(im) | ||||||
|  | @ -238,7 +236,7 @@ try: | ||||||
|         img_grey = Image.new("L", (100, 100)) |         img_grey = Image.new("L", (100, 100)) | ||||||
|         draw = ImageDraw.Draw(img_grey) |         draw = ImageDraw.Draw(img_grey) | ||||||
|         word = "testing" |         word = "testing" | ||||||
|             font = ImageFont.truetype(FONT_PATH, FONT_SIZE) |         font = self.get_font() | ||||||
| 
 | 
 | ||||||
|         orientation = Image.ROTATE_90 |         orientation = Image.ROTATE_90 | ||||||
|         transposed_font = ImageFont.TransposedFont( |         transposed_font = ImageFont.TransposedFont( | ||||||
|  | @ -261,7 +259,7 @@ try: | ||||||
|         img_grey = Image.new("L", (100, 100)) |         img_grey = Image.new("L", (100, 100)) | ||||||
|         draw = ImageDraw.Draw(img_grey) |         draw = ImageDraw.Draw(img_grey) | ||||||
|         word = "testing" |         word = "testing" | ||||||
|             font = ImageFont.truetype(FONT_PATH, FONT_SIZE) |         font = self.get_font() | ||||||
| 
 | 
 | ||||||
|         orientation = None |         orientation = None | ||||||
|         transposed_font = ImageFont.TransposedFont( |         transposed_font = ImageFont.TransposedFont( | ||||||
|  | @ -282,7 +280,7 @@ try: | ||||||
|     def test_rotated_transposed_font_get_mask(self): |     def test_rotated_transposed_font_get_mask(self): | ||||||
|         # Arrange |         # Arrange | ||||||
|         text = "mask this" |         text = "mask this" | ||||||
|             font = ImageFont.truetype(FONT_PATH, FONT_SIZE) |         font = self.get_font() | ||||||
|         orientation = Image.ROTATE_90 |         orientation = Image.ROTATE_90 | ||||||
|         transposed_font = ImageFont.TransposedFont( |         transposed_font = ImageFont.TransposedFont( | ||||||
|             font, orientation=orientation) |             font, orientation=orientation) | ||||||
|  | @ -296,7 +294,7 @@ try: | ||||||
|     def test_unrotated_transposed_font_get_mask(self): |     def test_unrotated_transposed_font_get_mask(self): | ||||||
|         # Arrange |         # Arrange | ||||||
|         text = "mask this" |         text = "mask this" | ||||||
|             font = ImageFont.truetype(FONT_PATH, FONT_SIZE) |         font = self.get_font() | ||||||
|         orientation = None |         orientation = None | ||||||
|         transposed_font = ImageFont.TransposedFont( |         transposed_font = ImageFont.TransposedFont( | ||||||
|             font, orientation=orientation) |             font, orientation=orientation) | ||||||
|  | @ -309,7 +307,7 @@ try: | ||||||
| 
 | 
 | ||||||
|     def test_free_type_font_get_name(self): |     def test_free_type_font_get_name(self): | ||||||
|         # Arrange |         # Arrange | ||||||
|             font = ImageFont.truetype(FONT_PATH, FONT_SIZE) |         font = self.get_font() | ||||||
| 
 | 
 | ||||||
|         # Act |         # Act | ||||||
|         name = font.getname() |         name = font.getname() | ||||||
|  | @ -319,7 +317,7 @@ try: | ||||||
| 
 | 
 | ||||||
|     def test_free_type_font_get_metrics(self): |     def test_free_type_font_get_metrics(self): | ||||||
|         # Arrange |         # Arrange | ||||||
|             font = ImageFont.truetype(FONT_PATH, FONT_SIZE) |         font = self.get_font() | ||||||
| 
 | 
 | ||||||
|         # Act |         # Act | ||||||
|         ascent, descent = font.getmetrics() |         ascent, descent = font.getmetrics() | ||||||
|  | @ -331,7 +329,7 @@ try: | ||||||
| 
 | 
 | ||||||
|     def test_free_type_font_get_offset(self): |     def test_free_type_font_get_offset(self): | ||||||
|         # Arrange |         # Arrange | ||||||
|             font = ImageFont.truetype(FONT_PATH, FONT_SIZE) |         font = self.get_font() | ||||||
|         text = "offset this" |         text = "offset this" | ||||||
| 
 | 
 | ||||||
|         # Act |         # Act | ||||||
|  | @ -342,7 +340,7 @@ try: | ||||||
| 
 | 
 | ||||||
|     def test_free_type_font_get_mask(self): |     def test_free_type_font_get_mask(self): | ||||||
|         # Arrange |         # Arrange | ||||||
|             font = ImageFont.truetype(FONT_PATH, FONT_SIZE) |         font = self.get_font() | ||||||
|         text = "mask this" |         text = "mask this" | ||||||
| 
 | 
 | ||||||
|         # Act |         # Act | ||||||
|  | @ -379,12 +377,12 @@ try: | ||||||
|         # Make a copy of FreeTypeFont so we can patch the original |         # Make a copy of FreeTypeFont so we can patch the original | ||||||
|         free_type_font = copy.deepcopy(ImageFont.FreeTypeFont) |         free_type_font = copy.deepcopy(ImageFont.FreeTypeFont) | ||||||
|         with SimplePatcher(ImageFont, '_FreeTypeFont', free_type_font): |         with SimplePatcher(ImageFont, '_FreeTypeFont', free_type_font): | ||||||
|                 def loadable_font(filepath, size, index, encoding): |             def loadable_font(filepath, size, index, encoding, *args, **kwargs): | ||||||
|                 if filepath == path_to_fake: |                 if filepath == path_to_fake: | ||||||
|                     return ImageFont._FreeTypeFont(FONT_PATH, size, index, |                     return ImageFont._FreeTypeFont(FONT_PATH, size, index, | ||||||
|                                                        encoding) |                                                    encoding, *args, **kwargs) | ||||||
|                 return ImageFont._FreeTypeFont(filepath, size, index, |                 return ImageFont._FreeTypeFont(filepath, size, index, | ||||||
|                                                    encoding) |                                                encoding, *args, **kwargs) | ||||||
|             with SimplePatcher(ImageFont, 'FreeTypeFont', loadable_font): |             with SimplePatcher(ImageFont, 'FreeTypeFont', loadable_font): | ||||||
|                 font = ImageFont.truetype(fontname) |                 font = ImageFont.truetype(fontname) | ||||||
|                 # Make sure it's loaded |                 # Make sure it's loaded | ||||||
|  | @ -450,7 +448,7 @@ try: | ||||||
| 
 | 
 | ||||||
|     def test_imagefont_getters(self): |     def test_imagefont_getters(self): | ||||||
|         # Arrange |         # Arrange | ||||||
|             t = ImageFont.truetype(FONT_PATH, FONT_SIZE) |         t = self.get_font() | ||||||
| 
 | 
 | ||||||
|         # Act / Assert |         # Act / Assert | ||||||
|         self.assertEqual(t.getmetrics(), (16, 4)) |         self.assertEqual(t.getmetrics(), (16, 4)) | ||||||
|  | @ -467,11 +465,9 @@ try: | ||||||
|         self.assertEqual(t.getsize('a'), (12, 16)) |         self.assertEqual(t.getsize('a'), (12, 16)) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| except ImportError: | @unittest.skipUnless(HAS_RAQM, "Raqm not Available") | ||||||
|     class TestImageFont(PillowTestCase): | class TestImageFont_RaqmLayout(TestImageFont): | ||||||
|         def test_skip(self): |     LAYOUT_ENGINE = ImageFont.LAYOUT_RAQM | ||||||
|             self.skipTest("ImportError") |  | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| if __name__ == '__main__': | if __name__ == '__main__': | ||||||
|     unittest.main() |     unittest.main() | ||||||
|  |  | ||||||
							
								
								
									
										133
									
								
								Tests/test_imagefontctl.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						|  | @ -0,0 +1,133 @@ | ||||||
|  | # -*- coding: utf-8 -*- | ||||||
|  | from helper import unittest, PillowTestCase | ||||||
|  | from PIL import Image, ImageDraw, ImageFont, features | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | FONT_SIZE = 20 | ||||||
|  | FONT_PATH = "Tests/fonts/DejaVuSans.ttf" | ||||||
|  | 
 | ||||||
|  | @unittest.skipUnless(features.check('raqm'), "Raqm Library is not installed.") | ||||||
|  | class TestImagecomplextext(PillowTestCase): | ||||||
|  | 
 | ||||||
|  |     def test_english(self): | ||||||
|  |         #smoke test, this should not fail | ||||||
|  |         ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) | ||||||
|  |         im = Image.new(mode='RGB', size=(300, 100)) | ||||||
|  |         draw = ImageDraw.Draw(im) | ||||||
|  |         draw.text((0, 0), 'TEST', font=ttf, fill=500, direction='ltr') | ||||||
|  |          | ||||||
|  |      | ||||||
|  |     def test_complex_text(self): | ||||||
|  |         ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) | ||||||
|  | 
 | ||||||
|  |         im = Image.new(mode='RGB', size=(300, 100)) | ||||||
|  |         draw = ImageDraw.Draw(im) | ||||||
|  |         draw.text((0, 0), 'اهلا عمان', font=ttf, fill=500) | ||||||
|  | 
 | ||||||
|  |         target = 'Tests/images/test_text.png' | ||||||
|  |         target_img = Image.open(target) | ||||||
|  | 
 | ||||||
|  |         self.assert_image_similar(im, target_img, .5) | ||||||
|  | 
 | ||||||
|  |     def test_y_offset(self): | ||||||
|  |         ttf = ImageFont.truetype("Tests/fonts/NotoNastaliqUrdu-Regular.ttf", FONT_SIZE) | ||||||
|  | 
 | ||||||
|  |         im = Image.new(mode='RGB', size=(300, 100)) | ||||||
|  |         draw = ImageDraw.Draw(im) | ||||||
|  |         draw.text((0, 0), 'العالم العربي', font=ttf, fill=500) | ||||||
|  | 
 | ||||||
|  |         target = 'Tests/images/test_y_offset.png' | ||||||
|  |         target_img = Image.open(target) | ||||||
|  | 
 | ||||||
|  |         self.assert_image_similar(im, target_img, 1.7) | ||||||
|  | 
 | ||||||
|  |     def test_complex_unicode_text(self): | ||||||
|  |         ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) | ||||||
|  | 
 | ||||||
|  |         im = Image.new(mode='RGB', size=(300, 100)) | ||||||
|  |         draw = ImageDraw.Draw(im) | ||||||
|  |         draw.text((0, 0), 'السلام عليكم', font=ttf, fill=500) | ||||||
|  | 
 | ||||||
|  |         target = 'Tests/images/test_complex_unicode_text.png' | ||||||
|  |         target_img = Image.open(target) | ||||||
|  | 
 | ||||||
|  |         self.assert_image_similar(im, target_img, .5) | ||||||
|  | 
 | ||||||
|  |     def test_text_direction_rtl(self): | ||||||
|  |         ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) | ||||||
|  | 
 | ||||||
|  |         im = Image.new(mode='RGB', size=(300, 100)) | ||||||
|  |         draw = ImageDraw.Draw(im) | ||||||
|  |         draw.text((0, 0), 'English عربي', font=ttf, fill=500, direction='rtl') | ||||||
|  | 
 | ||||||
|  |         target = 'Tests/images/test_direction_rtl.png' | ||||||
|  |         target_img = Image.open(target) | ||||||
|  | 
 | ||||||
|  |         self.assert_image_similar(im, target_img, .5) | ||||||
|  | 
 | ||||||
|  |     def test_text_direction_ltr(self): | ||||||
|  |         ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) | ||||||
|  | 
 | ||||||
|  |         im = Image.new(mode='RGB', size=(300, 100)) | ||||||
|  |         draw = ImageDraw.Draw(im) | ||||||
|  |         draw.text((0, 0),  'سلطنة عمان Oman', font=ttf, fill=500, direction='ltr') | ||||||
|  | 
 | ||||||
|  |         target = 'Tests/images/test_direction_ltr.png' | ||||||
|  |         target_img = Image.open(target) | ||||||
|  | 
 | ||||||
|  |         self.assert_image_similar(im, target_img, .5) | ||||||
|  | 
 | ||||||
|  |     def test_text_direction_rtl2(self): | ||||||
|  |         ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) | ||||||
|  | 
 | ||||||
|  |         im = Image.new(mode='RGB', size=(300, 100)) | ||||||
|  |         draw = ImageDraw.Draw(im) | ||||||
|  |         draw.text((0, 0), 'Oman سلطنة عمان', font=ttf, fill=500, direction='rtl') | ||||||
|  | 
 | ||||||
|  |         target = 'Tests/images/test_direction_ltr.png' | ||||||
|  |         target_img = Image.open(target) | ||||||
|  | 
 | ||||||
|  |         self.assert_image_similar(im, target_img, .5) | ||||||
|  | 
 | ||||||
|  |     def test_ligature_features(self): | ||||||
|  |         ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) | ||||||
|  | 
 | ||||||
|  |         im = Image.new(mode='RGB', size=(300, 100)) | ||||||
|  |         draw = ImageDraw.Draw(im) | ||||||
|  |         draw.text((0, 0), 'filling', font=ttf, fill=500, features=['-liga']) | ||||||
|  |         target = 'Tests/images/test_ligature_features.png' | ||||||
|  |         target_img = Image.open(target) | ||||||
|  | 
 | ||||||
|  |         self.assert_image_similar(im, target_img, .5) | ||||||
|  | 
 | ||||||
|  |         liga_size = ttf.getsize('fi', features=['-liga']) | ||||||
|  |         self.assertEqual(liga_size,(13,19)) | ||||||
|  |          | ||||||
|  |     def test_kerning_features(self): | ||||||
|  |         ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) | ||||||
|  | 
 | ||||||
|  |         im = Image.new(mode='RGB', size=(300, 100)) | ||||||
|  |         draw = ImageDraw.Draw(im) | ||||||
|  |         draw.text((0, 0), 'TeToAV', font=ttf, fill=500, features=['-kern']) | ||||||
|  | 
 | ||||||
|  |         target = 'Tests/images/test_kerning_features.png' | ||||||
|  |         target_img = Image.open(target) | ||||||
|  | 
 | ||||||
|  |         self.assert_image_similar(im, target_img, .5) | ||||||
|  | 
 | ||||||
|  |     def test_arabictext_features(self): | ||||||
|  |         ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) | ||||||
|  | 
 | ||||||
|  |         im = Image.new(mode='RGB', size=(300, 100)) | ||||||
|  |         draw = ImageDraw.Draw(im) | ||||||
|  |         draw.text((0, 0), 'اللغة العربية', font=ttf, fill=500, features=['-fina','-init','-medi']) | ||||||
|  | 
 | ||||||
|  |         target = 'Tests/images/test_arabictext_features.png' | ||||||
|  |         target_img = Image.open(target) | ||||||
|  | 
 | ||||||
|  |         self.assert_image_similar(im, target_img, .5) | ||||||
|  | 
 | ||||||
|  | if __name__ == '__main__': | ||||||
|  |     unittest.main() | ||||||
|  | 
 | ||||||
|  | # End of file | ||||||
							
								
								
									
										368
									
								
								_imagingft.c
									
									
									
									
									
								
							
							
						
						|  | @ -41,6 +41,18 @@ | ||||||
| #define FT_ERRORDEF( e, v, s )  { e, s }, | #define FT_ERRORDEF( e, v, s )  { e, s }, | ||||||
| #define FT_ERROR_START_LIST  { | #define FT_ERROR_START_LIST  { | ||||||
| #define FT_ERROR_END_LIST    { 0, 0 } }; | #define FT_ERROR_END_LIST    { 0, 0 } }; | ||||||
|  | #ifdef HAVE_RAQM | ||||||
|  | #include <raqm.h> | ||||||
|  | #endif | ||||||
|  | 
 | ||||||
|  | #define LAYOUT_FALLBACK 0 | ||||||
|  | #define LAYOUT_RAQM 1 | ||||||
|  | 
 | ||||||
|  | typedef struct | ||||||
|  | { | ||||||
|  |   int index, x_offset, x_advance, y_offset; | ||||||
|  |   unsigned int cluster; | ||||||
|  | } GlyphInfo; | ||||||
| 
 | 
 | ||||||
| struct { | struct { | ||||||
|     int code; |     int code; | ||||||
|  | @ -58,6 +70,7 @@ typedef struct { | ||||||
|     PyObject_HEAD |     PyObject_HEAD | ||||||
|     FT_Face face; |     FT_Face face; | ||||||
|     unsigned char *font_bytes; |     unsigned char *font_bytes; | ||||||
|  |     int layout_engine; | ||||||
| } FontObject; | } FontObject; | ||||||
| 
 | 
 | ||||||
| static PyTypeObject Font_Type; | static PyTypeObject Font_Type; | ||||||
|  | @ -91,11 +104,13 @@ getfont(PyObject* self_, PyObject* args, PyObject* kw) | ||||||
|     char* filename = NULL; |     char* filename = NULL; | ||||||
|     int size; |     int size; | ||||||
|     int index = 0; |     int index = 0; | ||||||
|  |     int layout_engine = 0; | ||||||
|     unsigned char* encoding; |     unsigned char* encoding; | ||||||
|     unsigned char* font_bytes; |     unsigned char* font_bytes; | ||||||
|     int font_bytes_size = 0; |     int font_bytes_size = 0; | ||||||
|     static char* kwlist[] = { |     static char* kwlist[] = { | ||||||
|         "filename", "size", "index", "encoding", "font_bytes", NULL |         "filename", "size", "index", "encoding", "font_bytes",  | ||||||
|  |         "layout_engine", NULL | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     if (!library) { |     if (!library) { | ||||||
|  | @ -106,10 +121,10 @@ getfont(PyObject* self_, PyObject* args, PyObject* kw) | ||||||
|         return NULL; |         return NULL; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (!PyArg_ParseTupleAndKeywords(args, kw, "eti|iss#", kwlist, |     if (!PyArg_ParseTupleAndKeywords(args, kw, "eti|iss#i", kwlist, | ||||||
|                                      Py_FileSystemDefaultEncoding, &filename, |                                      Py_FileSystemDefaultEncoding, &filename, | ||||||
|                                      &size, &index, &encoding, &font_bytes, |                                      &size, &index, &encoding, &font_bytes, | ||||||
|                                      &font_bytes_size)) { |                                      &font_bytes_size, &layout_engine)) { | ||||||
|         return NULL; |         return NULL; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -121,6 +136,7 @@ getfont(PyObject* self_, PyObject* args, PyObject* kw) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     self->face = NULL; |     self->face = NULL; | ||||||
|  |     self->layout_engine = layout_engine; | ||||||
| 
 | 
 | ||||||
|     if (filename && font_bytes_size <= 0) { |     if (filename && font_bytes_size <= 0) { | ||||||
|         self->font_bytes = NULL; |         self->font_bytes = NULL; | ||||||
|  | @ -188,60 +204,288 @@ font_getchar(PyObject* string, int index, FT_ULong* char_out) | ||||||
|     return 0; |     return 0; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| static PyObject* | #ifdef HAVE_RAQM | ||||||
| font_getsize(FontObject* self, PyObject* args) | static size_t | ||||||
|  | text_layout_raqm(PyObject* string, FontObject* self, const char* dir, | ||||||
|  |             PyObject *features ,GlyphInfo **glyph_info, int mask) | ||||||
| { | { | ||||||
|     int i, x, y_max, y_min; |     int i = 0; | ||||||
|  |     raqm_t *rq; | ||||||
|  |     size_t count = 0; | ||||||
|  |     raqm_glyph_t *glyphs; | ||||||
|  |     raqm_direction_t direction; | ||||||
|  | 
 | ||||||
|  |     rq = raqm_create(); | ||||||
|  |     if (rq == NULL) { | ||||||
|  |         PyErr_SetString(PyExc_ValueError, "raqm_create() failed."); | ||||||
|  |         goto failed; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (PyUnicode_Check(string)) { | ||||||
|  |         Py_UNICODE *text = PyUnicode_AS_UNICODE(string); | ||||||
|  |         Py_ssize_t size = PyUnicode_GET_SIZE(string); | ||||||
|  |         if (!raqm_set_text(rq, (const uint32_t *)(text), size)) { | ||||||
|  |             PyErr_SetString(PyExc_ValueError, "raqm_set_text() failed"); | ||||||
|  |             goto failed; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | #if PY_VERSION_HEX < 0x03000000 | ||||||
|  |     else if (PyString_Check(string)) { | ||||||
|  |         char *text = PyString_AS_STRING(string); | ||||||
|  |         int size = PyString_GET_SIZE(string); | ||||||
|  |         if (!raqm_set_text_utf8(rq, text, size)) { | ||||||
|  |             PyErr_SetString(PyExc_ValueError, "raqm_set_text_utf8() failed"); | ||||||
|  |             goto failed; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | #endif | ||||||
|  |     else { | ||||||
|  |         PyErr_SetString(PyExc_TypeError, "expected string"); | ||||||
|  |         goto failed; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     direction = RAQM_DIRECTION_DEFAULT; | ||||||
|  |     if (dir) { | ||||||
|  |         if (strcmp(dir, "rtl") == 0) | ||||||
|  |             direction = RAQM_DIRECTION_RTL; | ||||||
|  |         else if (strcmp(dir, "ltr") == 0) | ||||||
|  |             direction = RAQM_DIRECTION_LTR; | ||||||
|  |         else if (strcmp(dir, "ttb") == 0) | ||||||
|  |             direction = RAQM_DIRECTION_TTB; | ||||||
|  |         else { | ||||||
|  |             PyErr_SetString(PyExc_ValueError, "direction must be either 'rtl', 'ltr' or 'ttb'"); | ||||||
|  |             goto failed; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (!raqm_set_par_direction(rq, direction)) { | ||||||
|  |         PyErr_SetString(PyExc_ValueError, "raqm_set_par_direction() failed"); | ||||||
|  |         goto failed; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (features != Py_None) { | ||||||
|  |         int len; | ||||||
|  |         PyObject *seq = PySequence_Fast(features, "expected a sequence"); | ||||||
|  |         if (!seq) { | ||||||
|  |             goto failed; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         len = PySequence_Size(seq); | ||||||
|  |         for (i = 0; i < len; i++) { | ||||||
|  |             PyObject *item = PySequence_Fast_GET_ITEM(seq, i); | ||||||
|  |             char *feature = NULL; | ||||||
|  |             Py_ssize_t size = 0; | ||||||
|  |             PyObject *bytes; | ||||||
|  | 
 | ||||||
|  | #if PY_VERSION_HEX >= 0x03000000 | ||||||
|  |             if (!PyUnicode_Check(item)) { | ||||||
|  | #else | ||||||
|  |             if (!PyUnicode_Check(item) && !PyString_Check(item)) { | ||||||
|  | #endif | ||||||
|  |                 PyErr_SetString(PyExc_TypeError, "expected a string"); | ||||||
|  |                 goto failed; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (PyUnicode_Check(item)) { | ||||||
|  |                 bytes = PyUnicode_AsUTF8String(item); | ||||||
|  |                 if (bytes == NULL) | ||||||
|  |                     goto failed; | ||||||
|  |                 feature = PyBytes_AS_STRING(bytes); | ||||||
|  |                 size = PyBytes_GET_SIZE(bytes); | ||||||
|  |             } | ||||||
|  | #if PY_VERSION_HEX < 0x03000000 | ||||||
|  |             else { | ||||||
|  |                 feature = PyString_AsString(item); | ||||||
|  |                 size = PyString_GET_SIZE(item); | ||||||
|  |             } | ||||||
|  | #endif | ||||||
|  |             if (!raqm_add_font_feature(rq, feature, size)) { | ||||||
|  |                 PyErr_SetString(PyExc_ValueError, "raqm_add_font_feature() failed"); | ||||||
|  |                 goto failed; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (!raqm_set_freetype_face(rq, self->face)) { | ||||||
|  |       PyErr_SetString(PyExc_RuntimeError, "raqm_set_freetype_face() failed."); | ||||||
|  |       goto failed; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (!raqm_layout (rq)) { | ||||||
|  |       PyErr_SetString(PyExc_RuntimeError, "raqm_layout() failed."); | ||||||
|  |       goto failed; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     glyphs = raqm_get_glyphs(rq, &count); | ||||||
|  |     if (glyphs == NULL) { | ||||||
|  |         PyErr_SetString(PyExc_ValueError, "raqm_get_glyphs() failed."); | ||||||
|  |         count = 0; | ||||||
|  |         goto failed; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     (*glyph_info) = PyMem_New(GlyphInfo, count); | ||||||
|  |     if ((*glyph_info) == NULL) { | ||||||
|  |         PyErr_SetString(PyExc_MemoryError, "PyMem_New() failed"); | ||||||
|  |         count = 0; | ||||||
|  |         goto failed; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     for (i = 0; i < count; i++) { | ||||||
|  |         (*glyph_info)[i].index = glyphs[i].index; | ||||||
|  |         (*glyph_info)[i].x_offset = glyphs[i].x_offset; | ||||||
|  |         (*glyph_info)[i].x_advance = glyphs[i].x_advance; | ||||||
|  |         (*glyph_info)[i].y_offset = glyphs[i].y_offset; | ||||||
|  |         (*glyph_info)[i].cluster = glyphs[i].cluster; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | failed: | ||||||
|  |     raqm_destroy (rq); | ||||||
|  |     return count; | ||||||
|  | } | ||||||
|  | #endif | ||||||
|  | 
 | ||||||
|  | static size_t | ||||||
|  | text_layout_fallback(PyObject* string, FontObject* self, const char* dir, | ||||||
|  |             PyObject *features ,GlyphInfo **glyph_info, int mask) | ||||||
|  | { | ||||||
|  |     int error, load_flags; | ||||||
|     FT_ULong ch; |     FT_ULong ch; | ||||||
|     FT_Face face; |     Py_ssize_t count; | ||||||
|     int xoffset, yoffset; |     FT_GlyphSlot glyph; | ||||||
|     FT_Bool kerning = FT_HAS_KERNING(self->face); |     FT_Bool kerning = FT_HAS_KERNING(self->face); | ||||||
|     FT_UInt last_index = 0; |     FT_UInt last_index = 0; | ||||||
|  |     int i; | ||||||
| 
 | 
 | ||||||
|     /* calculate size and bearing for a given string */ |     if (features != Py_None || dir != NULL) { | ||||||
| 
 |       PyErr_SetString(PyExc_KeyError, "setting text direction or font features is not supported without libraqm"); | ||||||
|     PyObject* string; |     } | ||||||
|     if (!PyArg_ParseTuple(args, "O:getsize", &string)) |  | ||||||
|         return NULL; |  | ||||||
| 
 |  | ||||||
| #if PY_VERSION_HEX >= 0x03000000 | #if PY_VERSION_HEX >= 0x03000000 | ||||||
|     if (!PyUnicode_Check(string)) { |     if (!PyUnicode_Check(string)) { | ||||||
| #else | #else | ||||||
|     if (!PyUnicode_Check(string) && !PyString_Check(string)) { |     if (!PyUnicode_Check(string) && !PyString_Check(string)) { | ||||||
| #endif | #endif | ||||||
|         PyErr_SetString(PyExc_TypeError, "expected string"); |         PyErr_SetString(PyExc_TypeError, "expected string"); | ||||||
|         return NULL; |         return 0; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     count = 0; | ||||||
|  |     while (font_getchar(string, count, &ch)) { | ||||||
|  |        count++; | ||||||
|  |     } | ||||||
|  |     if (count == 0) { | ||||||
|  |         return 0; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     (*glyph_info) = PyMem_New(GlyphInfo, count); | ||||||
|  |     if ((*glyph_info) == NULL) { | ||||||
|  |         PyErr_SetString(PyExc_MemoryError, "PyMem_New() failed"); | ||||||
|  |         return 0; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     load_flags = FT_LOAD_RENDER|FT_LOAD_NO_BITMAP; | ||||||
|  |     if (mask) { | ||||||
|  |         load_flags |= FT_LOAD_TARGET_MONO; | ||||||
|  |     } | ||||||
|  |     for (i = 0; font_getchar(string, i, &ch); i++) { | ||||||
|  |         (*glyph_info)[i].index = FT_Get_Char_Index(self->face, ch); | ||||||
|  |         error = FT_Load_Glyph(self->face, (*glyph_info)[i].index, load_flags); | ||||||
|  |         if (error) { | ||||||
|  |             geterror(error); | ||||||
|  |             return 0; | ||||||
|  |         } | ||||||
|  |         glyph = self->face->glyph; | ||||||
|  |         (*glyph_info)[i].x_offset=0; | ||||||
|  |         (*glyph_info)[i].y_offset=0; | ||||||
|  |         if (kerning && last_index && (*glyph_info)[i].index) { | ||||||
|  |             FT_Vector delta; | ||||||
|  |             if (FT_Get_Kerning(self->face, last_index, (*glyph_info)[i].index, | ||||||
|  |                            ft_kerning_default,&delta) == 0) | ||||||
|  |             (*glyph_info)[i-1].x_advance += PIXEL(delta.x); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         (*glyph_info)[i].x_advance = glyph->metrics.horiAdvance; | ||||||
|  |         last_index = (*glyph_info)[i].index; | ||||||
|  |         (*glyph_info)[i].cluster = ch; | ||||||
|  |     } | ||||||
|  |     return count; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static size_t | ||||||
|  | text_layout(PyObject* string, FontObject* self, const char* dir, | ||||||
|  |             PyObject *features, GlyphInfo **glyph_info, int mask) | ||||||
|  | { | ||||||
|  |     size_t count; | ||||||
|  | #ifdef HAVE_RAQM | ||||||
|  |     if (self->layout_engine == LAYOUT_RAQM) { | ||||||
|  |         count = text_layout_raqm(string, self, dir, features, glyph_info,  mask); | ||||||
|  |     } else { | ||||||
|  |         count = text_layout_fallback(string, self, dir, features, glyph_info, mask); | ||||||
|  |     } | ||||||
|  | #else | ||||||
|  |     count = text_layout_fallback(string, self, dir, features, glyph_info, mask); | ||||||
|  | #endif | ||||||
|  |     return count; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static PyObject* | ||||||
|  | font_getsize(FontObject* self, PyObject* args) | ||||||
|  | { | ||||||
|  |     int i, x, y_max, y_min; | ||||||
|  |     FT_Face face; | ||||||
|  |     int xoffset, yoffset; | ||||||
|  |     const char *dir = NULL; | ||||||
|  |     size_t count; | ||||||
|  |     GlyphInfo *glyph_info = NULL; | ||||||
|  |     PyObject *features = Py_None; | ||||||
|  | 
 | ||||||
|  |     /* calculate size and bearing for a given string */ | ||||||
|  | 
 | ||||||
|  |     PyObject* string; | ||||||
|  |     if (!PyArg_ParseTuple(args, "O|zO:getsize", &string, &dir, &features)) | ||||||
|  |         return NULL; | ||||||
|  | 
 | ||||||
|     face = NULL; |     face = NULL; | ||||||
|     xoffset = yoffset = 0; |     xoffset = yoffset = 0; | ||||||
|     y_max = y_min = 0; |     y_max = y_min = 0; | ||||||
| 
 | 
 | ||||||
|     for (x = i = 0; font_getchar(string, i, &ch); i++) { |     count = text_layout(string, self, dir, features, &glyph_info, 0); | ||||||
|  |     if (count == 0) | ||||||
|  |         return NULL; | ||||||
|  | 
 | ||||||
|  |     for (x = i = 0; i < count; i++) { | ||||||
|         int index, error; |         int index, error; | ||||||
|         FT_BBox bbox; |         FT_BBox bbox; | ||||||
|         FT_Glyph glyph; |         FT_Glyph glyph; | ||||||
|         face = self->face; |         face = self->face; | ||||||
|         index = FT_Get_Char_Index(face, ch); |         index = glyph_info[i].index; | ||||||
|         if (kerning && last_index && index) { |  | ||||||
|             FT_Vector delta; |  | ||||||
|             FT_Get_Kerning(self->face, last_index, index, ft_kerning_default, |  | ||||||
|                            &delta); |  | ||||||
|             x += delta.x; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         /* Note: bitmap fonts within ttf fonts do not work, see #891/pr#960
 |         /* Note: bitmap fonts within ttf fonts do not work, see #891/pr#960
 | ||||||
|          *   Yifu Yu<root@jackyyf.com>, 2014-10-15 |          *   Yifu Yu<root@jackyyf.com>, 2014-10-15 | ||||||
|          */ |          */ | ||||||
|         error = FT_Load_Glyph(face, index, FT_LOAD_DEFAULT|FT_LOAD_NO_BITMAP); |         error = FT_Load_Glyph(face, index, FT_LOAD_DEFAULT|FT_LOAD_NO_BITMAP); | ||||||
|         if (error) |         if (error) | ||||||
|             return geterror(error); |             return geterror(error); | ||||||
|         if (i == 0) | 
 | ||||||
|  |         if (i == 0 && face->glyph->metrics.horiBearingX < 0) { | ||||||
|             xoffset = face->glyph->metrics.horiBearingX; |             xoffset = face->glyph->metrics.horiBearingX; | ||||||
|         x += face->glyph->metrics.horiAdvance; |             x -= xoffset; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         x += glyph_info[i].x_advance; | ||||||
|  | 
 | ||||||
|  |         if (i == count - 1) | ||||||
|  |         { | ||||||
|  |             int offset; | ||||||
|  |             offset = glyph_info[i].x_advance - | ||||||
|  |                     face->glyph->metrics.width - | ||||||
|  |                     face->glyph->metrics.horiBearingX; | ||||||
|  |             if (offset < 0) | ||||||
|  |                 x -= offset; | ||||||
|  |         } | ||||||
| 
 | 
 | ||||||
|         FT_Get_Glyph(face->glyph, &glyph); |         FT_Get_Glyph(face->glyph, &glyph); | ||||||
|         FT_Glyph_Get_CBox(glyph, FT_GLYPH_BBOX_SUBPIXELS, &bbox); |         FT_Glyph_Get_CBox(glyph, FT_GLYPH_BBOX_SUBPIXELS, &bbox); | ||||||
|  |         bbox.yMax -= glyph_info[i].y_offset; | ||||||
|  |         bbox.yMin -= glyph_info[i].y_offset; | ||||||
|         if (bbox.yMax > y_max) |         if (bbox.yMax > y_max) | ||||||
|             y_max = bbox.yMax; |             y_max = bbox.yMax; | ||||||
|         if (bbox.yMin < y_min) |         if (bbox.yMin < y_min) | ||||||
|  | @ -251,23 +495,16 @@ font_getsize(FontObject* self, PyObject* args) | ||||||
|         if (face->glyph->metrics.horiBearingY > yoffset) |         if (face->glyph->metrics.horiBearingY > yoffset) | ||||||
|             yoffset = face->glyph->metrics.horiBearingY; |             yoffset = face->glyph->metrics.horiBearingY; | ||||||
| 
 | 
 | ||||||
|         last_index = index; |  | ||||||
|         FT_Done_Glyph(glyph); |         FT_Done_Glyph(glyph); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (face) { |     if (face) { | ||||||
|         int offset; | 
 | ||||||
|         /* left bearing */ |         /* left bearing */ | ||||||
|         if (xoffset < 0) |         if (xoffset < 0) | ||||||
|             x -= xoffset; |             x -= xoffset; | ||||||
|         else |         else | ||||||
|             xoffset = 0; |             xoffset = 0; | ||||||
|         /* right bearing */ |  | ||||||
|         offset = face->glyph->metrics.horiAdvance - |  | ||||||
|             face->glyph->metrics.width - |  | ||||||
|             face->glyph->metrics.horiBearingX; |  | ||||||
|         if (offset < 0) |  | ||||||
|             x -= offset; |  | ||||||
|         /* difference between the font ascender and the distance of
 |         /* difference between the font ascender and the distance of
 | ||||||
|          * the baseline from the top */ |          * the baseline from the top */ | ||||||
|         yoffset = PIXEL(self->face->size->metrics.ascender - yoffset); |         yoffset = PIXEL(self->face->size->metrics.ascender - yoffset); | ||||||
|  | @ -329,11 +566,7 @@ font_render(FontObject* self, PyObject* args) | ||||||
|     int index, error, ascender; |     int index, error, ascender; | ||||||
|     int load_flags; |     int load_flags; | ||||||
|     unsigned char *source; |     unsigned char *source; | ||||||
|     FT_ULong ch; |  | ||||||
|     FT_GlyphSlot glyph; |     FT_GlyphSlot glyph; | ||||||
|     FT_Bool kerning = FT_HAS_KERNING(self->face); |  | ||||||
|     FT_UInt last_index = 0; |  | ||||||
| 
 |  | ||||||
|     /* render string into given buffer (the buffer *must* have
 |     /* render string into given buffer (the buffer *must* have
 | ||||||
|        the right size, or this will crash) */ |        the right size, or this will crash) */ | ||||||
|     PyObject* string; |     PyObject* string; | ||||||
|  | @ -341,15 +574,18 @@ font_render(FontObject* self, PyObject* args) | ||||||
|     int mask = 0; |     int mask = 0; | ||||||
|     int temp; |     int temp; | ||||||
|     int xx, x0, x1; |     int xx, x0, x1; | ||||||
|     if (!PyArg_ParseTuple(args, "On|i:render", &string, &id, &mask)) |     const char *dir = NULL; | ||||||
|         return NULL; |     size_t count; | ||||||
|  |     GlyphInfo *glyph_info; | ||||||
|  |     PyObject *features = NULL; | ||||||
| 
 | 
 | ||||||
| #if PY_VERSION_HEX >= 0x03000000 |     if (!PyArg_ParseTuple(args, "On|izO:render", &string,  &id, &mask, &dir, &features)) { | ||||||
|     if (!PyUnicode_Check(string)) { |         return NULL; | ||||||
| #else |     } | ||||||
|     if (!PyUnicode_Check(string) && !PyString_Check(string)) { | 
 | ||||||
| #endif |     glyph_info = NULL; | ||||||
|         PyErr_SetString(PyExc_TypeError, "expected string"); |     count = text_layout(string, self, dir, features, &glyph_info, mask); | ||||||
|  |     if (count == 0) { | ||||||
|         return NULL; |         return NULL; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -360,36 +596,37 @@ font_render(FontObject* self, PyObject* args) | ||||||
|         load_flags |= FT_LOAD_TARGET_MONO; |         load_flags |= FT_LOAD_TARGET_MONO; | ||||||
| 
 | 
 | ||||||
|     ascender = 0; |     ascender = 0; | ||||||
|     for (i = 0; font_getchar(string, i, &ch); i++) { |     for (i = 0; i < count; i++) { | ||||||
|         index = FT_Get_Char_Index(self->face, ch); |         index = glyph_info[i].index; | ||||||
|         error = FT_Load_Glyph(self->face, index, load_flags); |         error = FT_Load_Glyph(self->face, index, load_flags); | ||||||
|         if (error) |         if (error) | ||||||
|             return geterror(error); |             return geterror(error); | ||||||
|  | 
 | ||||||
|         glyph = self->face->glyph; |         glyph = self->face->glyph; | ||||||
|         temp = (glyph->bitmap.rows - glyph->bitmap_top); |         temp = (glyph->bitmap.rows - glyph->bitmap_top); | ||||||
|  |         temp -= PIXEL(glyph_info[i].y_offset); | ||||||
|         if (temp > ascender) |         if (temp > ascender) | ||||||
|             ascender = temp; |             ascender = temp; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     for (x = i = 0; font_getchar(string, i, &ch); i++) { |     for (x = i = 0; i < count; i++) { | ||||||
|         if (i == 0 && self->face->glyph->metrics.horiBearingX < 0) |         if (i == 0 && self->face->glyph->metrics.horiBearingX < 0) | ||||||
|             x = -PIXEL(self->face->glyph->metrics.horiBearingX); |             x = -self->face->glyph->metrics.horiBearingX; | ||||||
|         index = FT_Get_Char_Index(self->face, ch); |  | ||||||
|         if (kerning && last_index && index) { |  | ||||||
|             FT_Vector delta; |  | ||||||
|             FT_Get_Kerning(self->face, last_index, index, ft_kerning_default, |  | ||||||
|                            &delta); |  | ||||||
|             x += delta.x >> 6; |  | ||||||
|         } |  | ||||||
| 
 | 
 | ||||||
|  |         index = glyph_info[i].index; | ||||||
|         error = FT_Load_Glyph(self->face, index, load_flags); |         error = FT_Load_Glyph(self->face, index, load_flags); | ||||||
|         if (error) |         if (error) | ||||||
|             return geterror(error); |             return geterror(error); | ||||||
| 
 | 
 | ||||||
|  |         if (i == 0 && self->face->glyph->metrics.horiBearingX < 0) { | ||||||
|  |             x = -self->face->glyph->metrics.horiBearingX; | ||||||
|  |      } | ||||||
|  | 
 | ||||||
|         glyph = self->face->glyph; |         glyph = self->face->glyph; | ||||||
| 
 | 
 | ||||||
|         source = (unsigned char*) glyph->bitmap.buffer; |         source = (unsigned char*) glyph->bitmap.buffer; | ||||||
|         xx = x + glyph->bitmap_left; |         xx = PIXEL(x) + glyph->bitmap_left; | ||||||
|  |         xx += PIXEL(glyph_info[i].x_offset); | ||||||
|         x0 = 0; |         x0 = 0; | ||||||
|         x1 = glyph->bitmap.width; |         x1 = glyph->bitmap.width; | ||||||
|         if (xx < 0) |         if (xx < 0) | ||||||
|  | @ -401,6 +638,7 @@ font_render(FontObject* self, PyObject* args) | ||||||
|             /* use monochrome mask (on palette images, etc) */ |             /* use monochrome mask (on palette images, etc) */ | ||||||
|             for (y = 0; y < glyph->bitmap.rows; y++) { |             for (y = 0; y < glyph->bitmap.rows; y++) { | ||||||
|                 int yy = y + im->ysize - (PIXEL(glyph->metrics.horiBearingY) + ascender); |                 int yy = y + im->ysize - (PIXEL(glyph->metrics.horiBearingY) + ascender); | ||||||
|  |                 yy -= PIXEL(glyph_info[i].y_offset); | ||||||
|                 if (yy >= 0 && yy < im->ysize) { |                 if (yy >= 0 && yy < im->ysize) { | ||||||
|                     /* blend this glyph into the buffer */ |                     /* blend this glyph into the buffer */ | ||||||
|                     unsigned char *target = im->image8[yy] + xx; |                     unsigned char *target = im->image8[yy] + xx; | ||||||
|  | @ -420,8 +658,10 @@ font_render(FontObject* self, PyObject* args) | ||||||
|             /* use antialiased rendering */ |             /* use antialiased rendering */ | ||||||
|             for (y = 0; y < glyph->bitmap.rows; y++) { |             for (y = 0; y < glyph->bitmap.rows; y++) { | ||||||
|                 int yy = y + im->ysize - (PIXEL(glyph->metrics.horiBearingY) + ascender); |                 int yy = y + im->ysize - (PIXEL(glyph->metrics.horiBearingY) + ascender); | ||||||
|  |                 yy -= PIXEL(glyph_info[i].y_offset); | ||||||
|                 if (yy >= 0 && yy < im->ysize) { |                 if (yy >= 0 && yy < im->ysize) { | ||||||
|                     /* blend this glyph into the buffer */ |                     /* blend this glyph into the buffer */ | ||||||
|  | 
 | ||||||
|                     int i; |                     int i; | ||||||
|                     unsigned char *target = im->image8[yy] + xx; |                     unsigned char *target = im->image8[yy] + xx; | ||||||
|                     for (i = x0; i < x1; i++) { |                     for (i = x0; i < x1; i++) { | ||||||
|  | @ -432,10 +672,10 @@ font_render(FontObject* self, PyObject* args) | ||||||
|                 source += glyph->bitmap.pitch; |                 source += glyph->bitmap.pitch; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         x += PIXEL(glyph->metrics.horiAdvance); |         x += glyph_info[i].x_advance; | ||||||
|         last_index = index; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     PyMem_Del(glyph_info); | ||||||
|     Py_RETURN_NONE; |     Py_RETURN_NONE; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -593,6 +833,14 @@ setup_module(PyObject* m) { | ||||||
| #endif | #endif | ||||||
|     PyDict_SetItemString(d, "freetype2_version", v); |     PyDict_SetItemString(d, "freetype2_version", v); | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|  | #ifdef HAVE_RAQM | ||||||
|  |     v = PyBool_FromLong(1); | ||||||
|  | #else | ||||||
|  |     v = PyBool_FromLong(0); | ||||||
|  | #endif | ||||||
|  |     PyDict_SetItemString(d, "HAVE_RAQM", v); | ||||||
|  | 
 | ||||||
|     return 0; |     return 0; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -11,7 +11,8 @@ sudo apt-get -y install python-dev python-setuptools \ | ||||||
|     python3-dev python-virtualenv cmake |     python3-dev python-virtualenv cmake | ||||||
| sudo apt-get -y install libtiff5-dev libjpeg62-turbo-dev zlib1g-dev \ | sudo apt-get -y install libtiff5-dev libjpeg62-turbo-dev zlib1g-dev \ | ||||||
|      libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev \ |      libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev \ | ||||||
|      python-tk python3-tk |      python-tk python3-tk libharfbuzz-dev libfribidi-dev | ||||||
| 
 | 
 | ||||||
| ./install_openjpeg.sh | ./install_openjpeg.sh | ||||||
| ./install_imagequant.sh | ./install_imagequant.sh | ||||||
|  | ./install_raqm.sh | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| #!/bin/bash | #!/bin/sh | ||||||
| # Usage: ./download-and-extract.sh something.tar.gz https://example.com/something.tar.gz | # Usage: ./download-and-extract.sh something.tar.gz https://example.com/something.tar.gz | ||||||
| 
 | 
 | ||||||
| archive=$1 | archive=$1 | ||||||
|  |  | ||||||
|  | @ -15,4 +15,4 @@ sudo dnf install python-devel python3-devel python-virtualenv make gcc | ||||||
| 
 | 
 | ||||||
| sudo dnf install libtiff-devel libjpeg-devel zlib-devel freetype-devel \ | sudo dnf install libtiff-devel libjpeg-devel zlib-devel freetype-devel \ | ||||||
|     lcms2-devel libwebp-devel openjpeg2-devel tkinter python3-tkinter \ |     lcms2-devel libwebp-devel openjpeg2-devel tkinter python3-tkinter \ | ||||||
|     tcl-devel tk-devel |     tcl-devel tk-devel harfbuzz-devel fribidi-devel libraqm-devel | ||||||
|  | @ -4,8 +4,10 @@ | ||||||
| # Installs all of the dependencies for Pillow for Freebsd 10.x | # Installs all of the dependencies for Pillow for Freebsd 10.x | ||||||
| # for both system Pythons 2.7 and 3.4 | # for both system Pythons 2.7 and 3.4 | ||||||
| # | # | ||||||
| sudo pkg install python2 python3 py27-pip py27-virtualenv py27-setuptools27 | sudo pkg install python2 python3 py27-pip py27-virtualenv wget cmake | ||||||
| 
 | 
 | ||||||
| # Openjpeg fails badly using the openjpeg package. | # Openjpeg fails badly using the openjpeg package. | ||||||
| # I can't find a python3.4 version of tkinter | # I can't find a python3.4 version of tkinter | ||||||
| sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 py27-tkinter | sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 harfbuzz fribidi py27-tkinter | ||||||
|  | 
 | ||||||
|  | ./install_raqm_cmake.sh | ||||||
|  |  | ||||||
							
								
								
									
										14
									
								
								depends/install_raqm.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						|  | @ -0,0 +1,14 @@ | ||||||
|  | #!/usr/bin/env bash | ||||||
|  | # install raqm | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | archive=raqm-0.2.0 | ||||||
|  | 
 | ||||||
|  | ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/master/$archive.tar.gz | ||||||
|  | 
 | ||||||
|  | pushd $archive | ||||||
|  | 
 | ||||||
|  | ./configure --prefix=/usr && make -j4 && sudo make -j4 install | ||||||
|  | 
 | ||||||
|  | popd | ||||||
|  | 
 | ||||||
							
								
								
									
										18
									
								
								depends/install_raqm_cmake.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						|  | @ -0,0 +1,18 @@ | ||||||
|  | #!/usr/bin/env bash | ||||||
|  | # install raqm | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | archive=raqm-cmake-b517ba80 | ||||||
|  | 
 | ||||||
|  | ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/master/$archive.tar.gz | ||||||
|  | 
 | ||||||
|  | pushd $archive | ||||||
|  | 
 | ||||||
|  | mkdir build | ||||||
|  | cd build | ||||||
|  | cmake .. | ||||||
|  | make && sudo make install | ||||||
|  | cd .. | ||||||
|  | 
 | ||||||
|  | popd | ||||||
|  | 
 | ||||||
|  | @ -4,12 +4,13 @@ | ||||||
| # Installs all of the dependencies for Pillow for Ubuntu 14.04 | # Installs all of the dependencies for Pillow for Ubuntu 14.04 | ||||||
| # for both system Pythons 2.7 and 3.4 | # for both system Pythons 2.7 and 3.4 | ||||||
| # | # | ||||||
| 
 | sudo apt-get update | ||||||
| sudo apt-get -y install python-dev python-setuptools \ | sudo apt-get -y install python-dev python-setuptools \ | ||||||
|     python3-dev python-virtualenv cmake |     python3-dev python-virtualenv cmake | ||||||
| sudo apt-get -y install libtiff5-dev libjpeg8-dev zlib1g-dev \ | sudo apt-get -y install libtiff5-dev libjpeg8-dev zlib1g-dev \ | ||||||
|     libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev \ |     libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev \ | ||||||
|     python-tk python3-tk |     python-tk python3-tk libharfbuzz-dev libfribidi-dev | ||||||
| 
 | 
 | ||||||
| ./install_openjpeg.sh | ./install_openjpeg.sh | ||||||
| ./install_imagequant.sh | ./install_imagequant.sh | ||||||
|  | ./install_raqm.sh | ||||||
|  |  | ||||||
|  | @ -28,12 +28,6 @@ Basic Installation | ||||||
|     most common image formats. See :ref:`external-libraries` for a |     most common image formats. See :ref:`external-libraries` for a | ||||||
|     full list of external libraries supported. |     full list of external libraries supported. | ||||||
| 
 | 
 | ||||||
| .. note:: |  | ||||||
| 
 |  | ||||||
|    The basic installation works on Windows and macOS using the binaries |  | ||||||
|    from PyPI. Other installations require building from source as |  | ||||||
|    detailed below. |  | ||||||
| 
 |  | ||||||
| Install Pillow with :command:`pip`:: | Install Pillow with :command:`pip`:: | ||||||
| 
 | 
 | ||||||
|     $ pip install Pillow |     $ pip install Pillow | ||||||
|  | @ -72,11 +66,15 @@ except OpenJPEG:: | ||||||
| Linux Installation | Linux Installation | ||||||
| ^^^^^^^^^^^^^^^^^^ | ^^^^^^^^^^^^^^^^^^ | ||||||
| 
 | 
 | ||||||
| We do not provide binaries for Linux. Most major Linux distributions, | We provide binaries for Linux for each of the supported Python | ||||||
| including Fedora, Debian/Ubuntu and ArchLinux include Pillow in | versions in the manylinux wheel format. These include support for all | ||||||
| packages that previously contained PIL e.g. ``python-imaging``. Please | optional libraries except Raqm:: | ||||||
| consider using native operating system packages first to avoid | 
 | ||||||
| installation problems and/or missing library support later. |   $ pip install Pillow | ||||||
|  | 
 | ||||||
|  | Most major Linux distributions, including Fedora, Debian/Ubuntu and | ||||||
|  | ArchLinux also include Pillow in packages that previously contained | ||||||
|  | PIL e.g. ``python-imaging``. | ||||||
| 
 | 
 | ||||||
| FreeBSD Installation | FreeBSD Installation | ||||||
| ^^^^^^^^^^^^^^^^^^^^ | ^^^^^^^^^^^^^^^^^^^^ | ||||||
|  | @ -120,7 +118,9 @@ External Libraries | ||||||
| .. note:: | .. note:: | ||||||
| 
 | 
 | ||||||
|    There are scripts to install the dependencies for some operating |    There are scripts to install the dependencies for some operating | ||||||
|    systems included in the ``depends`` directory. |    systems included in the ``depends`` directory. Also see the | ||||||
|  |    Dockerfiles in our `docker images repo | ||||||
|  |    <https://github.com/python-pillow/docker-images>`_. | ||||||
| 
 | 
 | ||||||
| Many of Pillow's features require external libraries: | Many of Pillow's features require external libraries: | ||||||
| 
 | 
 | ||||||
|  | @ -170,6 +170,18 @@ Many of Pillow's features require external libraries: | ||||||
|   * Windows support: Libimagequant requires VS2013/MSVC 18 to compile, |   * Windows support: Libimagequant requires VS2013/MSVC 18 to compile, | ||||||
|     so it is unlikely to work with any Python prior to 3.5 on Windows. |     so it is unlikely to work with any Python prior to 3.5 on Windows. | ||||||
| 
 | 
 | ||||||
|  | * **libraqm** provides complex text layout support. | ||||||
|  | 
 | ||||||
|  |   * libraqm provides bidirectional text support (using FriBiDi), | ||||||
|  |     shaping (using HarfBuzz), and proper script itemization. As a | ||||||
|  |     result, Raqm can support most writing systems covered by Unicode. | ||||||
|  |   * libraqm depends on the following libraries: FreeType, HarfBuzz, | ||||||
|  |     FriBiDi, make sure that install them before install libraqm if not | ||||||
|  |     available as package in your system. | ||||||
|  |   * setting text direction or font features is not supported without | ||||||
|  |     libraqm. | ||||||
|  |   * Windows support: Raqm support is currently unsupported on Windows. | ||||||
|  | 
 | ||||||
| Once you have installed the prerequisites, run:: | Once you have installed the prerequisites, run:: | ||||||
| 
 | 
 | ||||||
|     $ pip install Pillow |     $ pip install Pillow | ||||||
|  | @ -201,14 +213,16 @@ Build Options | ||||||
| * Build flags: ``--disable-zlib``, ``--disable-jpeg``, | * Build flags: ``--disable-zlib``, ``--disable-jpeg``, | ||||||
|   ``--disable-tiff``, ``--disable-freetype``, ``--disable-tcl``, |   ``--disable-tiff``, ``--disable-freetype``, ``--disable-tcl``, | ||||||
|   ``--disable-tk``, ``--disable-lcms``, ``--disable-webp``, |   ``--disable-tk``, ``--disable-lcms``, ``--disable-webp``, | ||||||
|   ``--disable-webpmux``, ``--disable-jpeg2000``, ``--disable-imagequant``. |   ``--disable-webpmux``, ``--disable-jpeg2000``,  | ||||||
|  |   ``--disable-imagequant``, ``--disable-raqm``. | ||||||
|   Disable building the corresponding feature even if the development |   Disable building the corresponding feature even if the development | ||||||
|   libraries are present on the building machine. |   libraries are present on the building machine. | ||||||
| 
 | 
 | ||||||
| * Build flags: ``--enable-zlib``, ``--enable-jpeg``, | * Build flags: ``--enable-zlib``, ``--enable-jpeg``, | ||||||
|   ``--enable-tiff``, ``--enable-freetype``, ``--enable-tcl``, |   ``--enable-tiff``, ``--enable-freetype``, ``--enable-tcl``, | ||||||
|   ``--enable-tk``, ``--enable-lcms``, ``--enable-webp``, |   ``--enable-tk``, ``--enable-lcms``, ``--enable-webp``, | ||||||
|   ``--enable-webpmux``, ``--enable-jpeg2000``, ``--enable-imagequant``. |   ``--enable-webpmux``, ``--enable-jpeg2000``, | ||||||
|  |   ``--enable-imagequant``,  ``--enable-raqm``. | ||||||
|   Require that the corresponding feature is built. The build will raise |   Require that the corresponding feature is built. The build will raise | ||||||
|   an exception if the libraries are not found. Webpmux (WebP metadata) |   an exception if the libraries are not found. Webpmux (WebP metadata) | ||||||
|   relies on WebP support. Tcl and Tk also must be used together. |   relies on WebP support. Tcl and Tk also must be used together. | ||||||
|  | @ -247,7 +261,12 @@ The easiest way to install external libraries is via `Homebrew | ||||||
| 
 | 
 | ||||||
|     $ brew install libtiff libjpeg webp little-cms2 |     $ brew install libtiff libjpeg webp little-cms2 | ||||||
| 
 | 
 | ||||||
| Install Pillow with:: | To install libraqm on MaxOS use Homebrew to install its dependencies:: | ||||||
|  |     $ brew install freetype harfbuzz fribidi | ||||||
|  | 
 | ||||||
|  | Then see ``depends/install_raqm_cmake.sh`` to install libraqm. | ||||||
|  | 
 | ||||||
|  | Now install Pillow with:: | ||||||
| 
 | 
 | ||||||
|     $ pip install Pillow |     $ pip install Pillow | ||||||
| 
 | 
 | ||||||
|  | @ -277,7 +296,9 @@ Or for Python 3:: | ||||||
| 
 | 
 | ||||||
| Prerequisites are installed on **FreeBSD 10 or 11** with:: | Prerequisites are installed on **FreeBSD 10 or 11** with:: | ||||||
| 
 | 
 | ||||||
|     $ sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg |     $ sudo pkg install jpeg-turbo tiff webp lcms2 freetype2 openjpeg harfbuzz fribidi | ||||||
|  | 
 | ||||||
|  | Then see ``depends/install_raqm_cmake.sh`` to install libraqm. | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| Building on Linux | Building on Linux | ||||||
|  | @ -313,12 +334,15 @@ Prerequisites are installed on **Ubuntu 12.04 LTS** or **Raspian Wheezy | ||||||
| Prerequisites are installed on **Ubuntu 14.04 LTS** with:: | Prerequisites are installed on **Ubuntu 14.04 LTS** with:: | ||||||
| 
 | 
 | ||||||
|     $ sudo apt-get install libtiff5-dev libjpeg8-dev zlib1g-dev \ |     $ sudo apt-get install libtiff5-dev libjpeg8-dev zlib1g-dev \ | ||||||
|         libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python-tk |         libfreetype6-dev liblcms2-dev libwebp-dev libharfbuzz-dev libfribidi-dev \ | ||||||
|  |         tcl8.6-dev tk8.6-dev python-tk | ||||||
|  | 
 | ||||||
|  | Then see ``depends/install_raqm.sh`` to install libraqm. | ||||||
| 
 | 
 | ||||||
| Prerequisites are installed on **Fedora 23** with:: | Prerequisites are installed on **Fedora 23** with:: | ||||||
| 
 | 
 | ||||||
|     $ sudo dnf install libtiff-devel libjpeg-devel zlib-devel freetype-devel \ |     $ sudo dnf install libtiff-devel libjpeg-devel zlib-devel freetype-devel \ | ||||||
|         lcms2-devel libwebp-devel tcl-devel tk-devel |         lcms2-devel libwebp-devel tcl-devel tk-devel libraqm-devel | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -227,7 +227,7 @@ Methods | ||||||
| 
 | 
 | ||||||
|     Draw a shape. |     Draw a shape. | ||||||
| 
 | 
 | ||||||
| .. py:method:: PIL.ImageDraw.Draw.text(xy, text, fill=None, font=None, anchor=None, spacing=0, align="left") | .. py:method:: PIL.ImageDraw.Draw.text(xy, text, fill=None, font=None, anchor=None, spacing=0, align="left", direction=None, features=None) | ||||||
| 
 | 
 | ||||||
|     Draws the string at the given position. |     Draws the string at the given position. | ||||||
| 
 | 
 | ||||||
|  | @ -240,9 +240,28 @@ Methods | ||||||
|                     the number of pixels between lines. |                     the number of pixels between lines. | ||||||
|     :param align: If the text is passed on to multiline_text(), |     :param align: If the text is passed on to multiline_text(), | ||||||
|                   "left", "center" or "right". |                   "left", "center" or "right". | ||||||
|  |     :param direction: Direction of the text. It can be 'rtl' (right to | ||||||
|  |                       left), 'ltr' (left to right), 'ttb' (top to | ||||||
|  |                       bottom) or 'btt' (bottom to top). Requires | ||||||
|  |                       libraqm. | ||||||
| 
 | 
 | ||||||
|  |                       .. versionadded:: 4.2.0 | ||||||
| 
 | 
 | ||||||
| .. py:method:: PIL.ImageDraw.Draw.multiline_text(xy, text, fill=None, font=None, anchor=None, spacing=0, align="left") |     :param features: A list of OpenType font features to be used during text | ||||||
|  |                      layout. This is usually used to turn on optional | ||||||
|  |                      font features that are not enabled by default, | ||||||
|  |                      for example 'dlig' or 'ss01', but can be also | ||||||
|  |                      used to turn off default font features for | ||||||
|  |                      example '-liga' to disable ligatures or '-kern' | ||||||
|  |                      to disable kerning.  To get all supported | ||||||
|  |                      features, see | ||||||
|  |                      https://www.microsoft.com/typography/otspec/featurelist.htm | ||||||
|  |                      Requires libraqm. | ||||||
|  | 
 | ||||||
|  |                      .. versionadded:: 4.2.0 | ||||||
|  | 
 | ||||||
|  | .. py:method:: PIL.ImageDraw.Draw.multiline_text(xy, text, fill=None, font=None, anchor=None, spacing=0, align="left", | ||||||
|  |                                                  direction=None, features=None) | ||||||
| 
 | 
 | ||||||
|     Draws the string at the given position. |     Draws the string at the given position. | ||||||
| 
 | 
 | ||||||
|  | @ -252,8 +271,28 @@ Methods | ||||||
|     :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance. |     :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance. | ||||||
|     :param spacing: The number of pixels between lines. |     :param spacing: The number of pixels between lines. | ||||||
|     :param align: "left", "center" or "right". |     :param align: "left", "center" or "right". | ||||||
|  |     :param direction: Direction of the text. It can be 'rtl' (right to | ||||||
|  |                       left), 'ltr' (left to right), 'ttb' (top to | ||||||
|  |                       bottom) or 'btt' (bottom to top). Requires | ||||||
|  |                       libraqm. | ||||||
| 
 | 
 | ||||||
| .. py:method:: PIL.ImageDraw.Draw.textsize(text, font=None, spacing=0) |                       .. versionadded:: 4.2.0 | ||||||
|  | 
 | ||||||
|  |     :param features: A list of OpenType font features to be used during text | ||||||
|  |                      layout. This is usually used to turn on optional | ||||||
|  |                      font features that are not enabled by default, | ||||||
|  |                      for example 'dlig' or 'ss01', but can be also | ||||||
|  |                      used to turn off default font features for | ||||||
|  |                      example '-liga' to disable ligatures or '-kern' | ||||||
|  |                      to disable kerning.  To get all supported | ||||||
|  |                      features, see | ||||||
|  |                      https://www.microsoft.com/typography/otspec/featurelist.htm | ||||||
|  |                      Requires libraqm. | ||||||
|  | 
 | ||||||
|  |                      .. versionadded:: 4.2.0 | ||||||
|  | 
 | ||||||
|  | .. py:method:: PIL.ImageDraw.Draw.textsize(text, font=None, spacing=4, direction=None, | ||||||
|  |                            features=None) | ||||||
| 
 | 
 | ||||||
|     Return the size of the given string, in pixels. |     Return the size of the given string, in pixels. | ||||||
| 
 | 
 | ||||||
|  | @ -262,11 +301,51 @@ Methods | ||||||
|     :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance. |     :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance. | ||||||
|     :param spacing: If the text is passed on to multiline_textsize(), |     :param spacing: If the text is passed on to multiline_textsize(), | ||||||
|                     the number of pixels between lines. |                     the number of pixels between lines. | ||||||
|  |     :param direction: Direction of the text. It can be 'rtl' (right to | ||||||
|  |                       left), 'ltr' (left to right), 'ttb' (top to | ||||||
|  |                       bottom) or 'btt' (bottom to top). Requires | ||||||
|  |                       libraqm. | ||||||
| 
 | 
 | ||||||
| .. py:method:: PIL.ImageDraw.Draw.multiline_textsize(text, font=None, spacing=0) |                       .. versionadded:: 4.2.0 | ||||||
|  | 
 | ||||||
|  |     :param features: A list of OpenType font features to be used during text | ||||||
|  |                      layout. This is usually used to turn on optional | ||||||
|  |                      font features that are not enabled by default, | ||||||
|  |                      for example 'dlig' or 'ss01', but can be also | ||||||
|  |                      used to turn off default font features for | ||||||
|  |                      example '-liga' to disable ligatures or '-kern' | ||||||
|  |                      to disable kerning.  To get all supported | ||||||
|  |                      features, see | ||||||
|  |                      https://www.microsoft.com/typography/otspec/featurelist.htm | ||||||
|  |                      Requires libraqm. | ||||||
|  | 
 | ||||||
|  |                      .. versionadded:: 4.2.0 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | .. py:method:: PIL.ImageDraw.Draw.multiline_textsize(text, font=None, spacing=4, direction=None, | ||||||
|  |                            features=None) | ||||||
| 
 | 
 | ||||||
|     Return the size of the given string, in pixels. |     Return the size of the given string, in pixels. | ||||||
| 
 | 
 | ||||||
|     :param text: Text to be measured. |     :param text: Text to be measured. | ||||||
|     :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance. |     :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance. | ||||||
|     :param spacing: The number of pixels between lines. |     :param spacing: The number of pixels between lines. | ||||||
|  |     :param direction: Direction of the text. It can be 'rtl' (right to | ||||||
|  |                       left), 'ltr' (left to right), 'ttb' (top to | ||||||
|  |                       bottom) or 'btt' (bottom to top). Requires | ||||||
|  |                       libraqm. | ||||||
|  | 
 | ||||||
|  |                       .. versionadded:: 4.2.0 | ||||||
|  | 
 | ||||||
|  |     :param features: A list of OpenType font features to be used during text | ||||||
|  |                      layout. This is usually used to turn on optional | ||||||
|  |                      font features that are not enabled by default, | ||||||
|  |                      for example 'dlig' or 'ss01', but can be also | ||||||
|  |                      used to turn off default font features for | ||||||
|  |                      example '-liga' to disable ligatures or '-kern' | ||||||
|  |                      to disable kerning.  To get all supported | ||||||
|  |                      features, see | ||||||
|  |                      https://www.microsoft.com/typography/otspec/featurelist.htm | ||||||
|  |                      Requires libraqm. | ||||||
|  | 
 | ||||||
|  |                      .. versionadded:: 4.2.0 | ||||||
|  |  | ||||||
|  | @ -51,7 +51,7 @@ Methods | ||||||
| 
 | 
 | ||||||
|     :return: (width, height) |     :return: (width, height) | ||||||
| 
 | 
 | ||||||
| .. py:method:: PIL.ImageFont.ImageFont.getmask(text, mode='') | .. py:method:: PIL.ImageFont.ImageFont.getmask(text, mode='', direction=None, features=[]) | ||||||
| 
 | 
 | ||||||
|     Create a bitmap for the text. |     Create a bitmap for the text. | ||||||
| 
 | 
 | ||||||
|  | @ -65,5 +65,26 @@ Methods | ||||||
|                  C-level implementations. |                  C-level implementations. | ||||||
| 
 | 
 | ||||||
|                  .. versionadded:: 1.1.5 |                  .. versionadded:: 1.1.5 | ||||||
|  | 
 | ||||||
|  |     :param direction: Direction of the text. It can be 'rtl' (right to | ||||||
|  |                       left), 'ltr' (left to right), 'ttb' (top to | ||||||
|  |                       bottom) or 'btt' (bottom to top). Requires | ||||||
|  |                       libraqm. | ||||||
|  | 
 | ||||||
|  |                       .. versionadded:: 4.2.0 | ||||||
|  | 
 | ||||||
|  |     :param features: A list of OpenType font features to be used during text | ||||||
|  |                      layout. This is usually used to turn on optional | ||||||
|  |                      font features that are not enabled by default, | ||||||
|  |                      for example 'dlig' or 'ss01', but can be also | ||||||
|  |                      used to turn off default font features for | ||||||
|  |                      example '-liga' to disable ligatures or '-kern' | ||||||
|  |                      to disable kerning.  To get all supported | ||||||
|  |                      features, see | ||||||
|  |                      https://www.microsoft.com/typography/otspec/featurelist.htm | ||||||
|  |                      Requires libraqm. | ||||||
|  | 
 | ||||||
|  |                      .. versionadded:: 4.2.0 | ||||||
|  | 
 | ||||||
|     :return: An internal PIL storage memory instance as defined by the |     :return: An internal PIL storage memory instance as defined by the | ||||||
|              :py:mod:`PIL.Image.core` interface module. |              :py:mod:`PIL.Image.core` interface module. | ||||||
|  |  | ||||||
|  | @ -1,11 +1,26 @@ | ||||||
| 4.2.0 | 4.2.0 | ||||||
| ----- | ----- | ||||||
| 
 | 
 | ||||||
|  | Added Complex Text Rendering | ||||||
|  | ============================ | ||||||
|  | 
 | ||||||
|  | Pillow now supports complex text rendering for scripts requiring glyph | ||||||
|  | composition and bidirectional flow. This optional feature adds three | ||||||
|  | dependencies: harfbuzz, fribidi, and raqm. See the install | ||||||
|  | documentation for further details. This feature is tested and works on | ||||||
|  | Un*x and Mac, but has not yet been built on Windows platforms. | ||||||
|  | 
 | ||||||
| Removed Deprecated Items | Removed Deprecated Items | ||||||
| ======================== | ======================== | ||||||
| 
 | 
 | ||||||
| Several deprecated items have been removed. | Several deprecated items have been removed. | ||||||
| 
 | 
 | ||||||
| * The methods :py:meth:`PIL.ImageWin.Dib.fromstring`, :py:meth:`PIL.ImageWin.Dib.tostring` and :py:meth:`PIL.TiffImagePlugin.ImageFileDirectory_v2.as_dict` have been removed. | * The methods :py:meth:`PIL.ImageWin.Dib.fromstring`, | ||||||
|  |   :py:meth:`PIL.ImageWin.Dib.tostring` and | ||||||
|  |   :py:meth:`PIL.TiffImagePlugin.ImageFileDirectory_v2.as_dict` have | ||||||
|  |   been removed. | ||||||
| 
 | 
 | ||||||
| * Before Pillow 4.2.0, attempting to save an RGBA image as JPEG would discard the alpha channel. From Pillow 3.4.0, a deprecation warning was shown. From Pillow 4.2.0, the deprecation warning is removed and an :py:exc:`IOError` is raised. | * Before Pillow 4.2.0, attempting to save an RGBA image as JPEG would | ||||||
|  |   discard the alpha channel. From Pillow 3.4.0, a deprecation warning | ||||||
|  |   was shown. From Pillow 4.2.0, the deprecation warning is removed and | ||||||
|  |   an :py:exc:`IOError` is raised. | ||||||
|  |  | ||||||
|  | @ -183,7 +183,8 @@ if __name__ == "__main__": | ||||||
|         ("jpg", "JPEG"), |         ("jpg", "JPEG"), | ||||||
|         ("jpg_2000", "OPENJPEG (JPEG2000)"), |         ("jpg_2000", "OPENJPEG (JPEG2000)"), | ||||||
|         ("zlib", "ZLIB (PNG/ZIP)"), |         ("zlib", "ZLIB (PNG/ZIP)"), | ||||||
|         ("libtiff", "LIBTIFF") |         ("libtiff", "LIBTIFF"), | ||||||
|  |         ("raqm", "RAQM (Bidirectional Text)") | ||||||
|     ]: |     ]: | ||||||
|         if features.check(name): |         if features.check(name): | ||||||
|             print("---", feature, "support ok") |             print("---", feature, "support ok") | ||||||
|  |  | ||||||
							
								
								
									
										24
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						|  | @ -119,7 +119,7 @@ IMAGEQUANT_ROOT = None | ||||||
| TIFF_ROOT = None | TIFF_ROOT = None | ||||||
| FREETYPE_ROOT = None | FREETYPE_ROOT = None | ||||||
| LCMS_ROOT = None | LCMS_ROOT = None | ||||||
| 
 | RAQM_ROOT = None | ||||||
| 
 | 
 | ||||||
| def _pkg_config(name): | def _pkg_config(name): | ||||||
|     try: |     try: | ||||||
|  | @ -137,7 +137,7 @@ def _pkg_config(name): | ||||||
| 
 | 
 | ||||||
| class pil_build_ext(build_ext): | class pil_build_ext(build_ext): | ||||||
|     class feature: |     class feature: | ||||||
|         features = ['zlib', 'jpeg', 'tiff', 'freetype', 'lcms', 'webp', |         features = ['zlib', 'jpeg', 'tiff', 'freetype', 'raqm', 'lcms', 'webp', | ||||||
|                     'webpmux', 'jpeg2000', 'imagequant'] |                     'webpmux', 'jpeg2000', 'imagequant'] | ||||||
| 
 | 
 | ||||||
|         required = {'jpeg', 'zlib'} |         required = {'jpeg', 'zlib'} | ||||||
|  | @ -522,6 +522,14 @@ class pil_build_ext(build_ext): | ||||||
|                     if subdir: |                     if subdir: | ||||||
|                         _add_directory(self.compiler.include_dirs, subdir, 0) |                         _add_directory(self.compiler.include_dirs, subdir, 0) | ||||||
| 
 | 
 | ||||||
|  |         if feature.want('raqm'): | ||||||
|  |             _dbg('Looking for raqm') | ||||||
|  |             if _find_include_file(self, "raqm.h"): | ||||||
|  |                 if _find_library_file(self, "raqm") and \ | ||||||
|  |                    _find_library_file(self, "harfbuzz") and \ | ||||||
|  |                    _find_library_file(self, "fribidi"): | ||||||
|  |                     feature.raqm = ["raqm", "harfbuzz", "fribidi"] | ||||||
|  | 
 | ||||||
|         if feature.want('lcms'): |         if feature.want('lcms'): | ||||||
|             _dbg('Looking for lcms') |             _dbg('Looking for lcms') | ||||||
|             if _find_include_file(self, "lcms2.h"): |             if _find_include_file(self, "lcms2.h"): | ||||||
|  | @ -605,9 +613,14 @@ class pil_build_ext(build_ext): | ||||||
|         # additional libraries |         # additional libraries | ||||||
| 
 | 
 | ||||||
|         if feature.freetype: |         if feature.freetype: | ||||||
|             exts.append(Extension("PIL._imagingft", |             libs = ["freetype"] | ||||||
|                                   ["_imagingft.c"], |             defs = [] | ||||||
|                                   libraries=["freetype"])) |             if feature.raqm: | ||||||
|  |                 libs.extend(feature.raqm) | ||||||
|  |                 defs.append(('HAVE_RAQM', None)) | ||||||
|  |             exts.append(Extension( | ||||||
|  |                 "PIL._imagingft", ["_imagingft.c"], libraries=libs, | ||||||
|  |                 define_macros=defs)) | ||||||
| 
 | 
 | ||||||
|         if feature.lcms: |         if feature.lcms: | ||||||
|             extra = [] |             extra = [] | ||||||
|  | @ -669,6 +682,7 @@ class pil_build_ext(build_ext): | ||||||
|             (feature.imagequant, "LIBIMAGEQUANT"), |             (feature.imagequant, "LIBIMAGEQUANT"), | ||||||
|             (feature.tiff, "LIBTIFF"), |             (feature.tiff, "LIBTIFF"), | ||||||
|             (feature.freetype, "FREETYPE2"), |             (feature.freetype, "FREETYPE2"), | ||||||
|  |             (feature.raqm, "RAQM"), | ||||||
|             (feature.lcms, "LITTLECMS2"), |             (feature.lcms, "LITTLECMS2"), | ||||||
|             (feature.webp, "WEBP"), |             (feature.webp, "WEBP"), | ||||||
|             (feature.webpmux, "WEBPMUX"), |             (feature.webpmux, "WEBPMUX"), | ||||||
|  |  | ||||||