@@ -29,6 +29,8 @@ def get_version():
2929NAME = "Pillow"
3030PILLOW_VERSION = get_version ()
3131FREETYPE_ROOT = None
32+ HARFBUZZ_ROOT = None
33+ FRIBIDI_ROOT = None
3234IMAGEQUANT_ROOT = None
3335JPEG2K_ROOT = None
3436JPEG_ROOT = None
@@ -228,6 +230,19 @@ def _find_library_file(self, library):
228230 return ret
229231
230232
233+ def _find_include_dir (self , dirname , include ):
234+ for directory in self .compiler .include_dirs :
235+ _dbg ("Checking for include file %s in %s" , (include , directory ))
236+ if os .path .isfile (os .path .join (directory , include )):
237+ _dbg ("Found %s in %s" , (include , directory ))
238+ return True
239+ subdir = os .path .join (directory , dirname )
240+ _dbg ("Checking for include file %s in %s" , (include , subdir ))
241+ if os .path .isfile (os .path .join (subdir , include )):
242+ _dbg ("Found %s in %s" , (include , subdir ))
243+ return subdir
244+
245+
231246def _cmd_exists (cmd ):
232247 return any (
233248 os .access (os .path .join (path , cmd ), os .X_OK )
@@ -267,6 +282,7 @@ class feature:
267282 "jpeg" ,
268283 "tiff" ,
269284 "freetype" ,
285+ "raqm" ,
270286 "lcms" ,
271287 "webp" ,
272288 "webpmux" ,
@@ -276,6 +292,7 @@ class feature:
276292 ]
277293
278294 required = {"jpeg" , "zlib" }
295+ vendor = set ()
279296
280297 def __init__ (self ):
281298 for f in self .features :
@@ -287,6 +304,9 @@ def require(self, feat):
287304 def want (self , feat ):
288305 return getattr (self , feat ) is None
289306
307+ def want_vendor (self , feat ):
308+ return feat in self .vendor
309+
290310 def __iter__ (self ):
291311 yield from self .features
292312
@@ -296,6 +316,10 @@ def __iter__(self):
296316 build_ext .user_options
297317 + [(f"disable-{ x } " , None , f"Disable support for { x } " ) for x in feature ]
298318 + [(f"enable-{ x } " , None , f"Enable support for { x } " ) for x in feature ]
319+ + [
320+ (f"vendor-{ x } " , None , f"Use vendored version of { x } " )
321+ for x in ("raqm" , "fribidi" )
322+ ]
299323 + [
300324 ("disable-platform-guessing" , None , "Disable platform guessing on Linux" ),
301325 ("debug" , None , "Debug logging" ),
@@ -310,6 +334,8 @@ def initialize_options(self):
310334 for x in self .feature :
311335 setattr (self , f"disable_{ x } " , None )
312336 setattr (self , f"enable_{ x } " , None )
337+ for x in ("raqm" , "fribidi" ):
338+ setattr (self , f"vendor_{ x } " , None )
313339
314340 def finalize_options (self ):
315341 build_ext .finalize_options (self )
@@ -334,18 +360,40 @@ def finalize_options(self):
334360 raise ValueError (
335361 f"Conflicting options: --enable-{ x } and --disable-{ x } "
336362 )
363+ if x == "freetype" :
364+ _dbg ("--disable-freetype implies --disable-raqm" )
365+ if getattr (self , "enable_raqm" ):
366+ raise ValueError (
367+ "Conflicting options: --enable-raqm and --disable-freetype"
368+ )
369+ setattr (self , "disable_raqm" , True )
337370 if getattr (self , f"enable_{ x } " ):
338371 _dbg ("Requiring %s" , x )
339372 self .feature .required .add (x )
373+ if x == "raqm" :
374+ _dbg ("--enable-raqm implies --enable-freetype" )
375+ self .feature .required .add ("freetype" )
376+ for x in ("raqm" , "fribidi" ):
377+ if getattr (self , f"vendor_{ x } " ):
378+ if getattr (self , "disable_raqm" ):
379+ raise ValueError (
380+ f"Conflicting options: --vendor-{ x } and --disable-raqm"
381+ )
382+ if x == "fribidi" and not getattr (self , "vendor_raqm" ):
383+ raise ValueError (
384+ f"Conflicting options: --vendor-{ x } and not --vendor-raqm"
385+ )
386+ _dbg ("Using vendored version of %s" , x )
387+ self .feature .vendor .add (x )
340388
341- def _update_extension (self , name , libraries , define_macros = None , include_dirs = None ):
389+ def _update_extension (self , name , libraries , define_macros = None , sources = None ):
342390 for extension in self .extensions :
343391 if extension .name == name :
344392 extension .libraries += libraries
345393 if define_macros is not None :
346394 extension .define_macros += define_macros
347- if include_dirs is not None :
348- extension .include_dirs += include_dirs
395+ if sources is not None :
396+ extension .sources += sources
349397 if FUZZING_BUILD :
350398 extension .language = "c++"
351399 extension .extra_link_args = ["--stdlib=libc++" ]
@@ -374,6 +422,8 @@ def build_extensions(self):
374422 TIFF_ROOT = ("libtiff-5" , "libtiff-4" ),
375423 ZLIB_ROOT = "zlib" ,
376424 FREETYPE_ROOT = "freetype2" ,
425+ HARFBUZZ_ROOT = "harfbuzz" ,
426+ FRIBIDI_ROOT = "fribidi" ,
377427 LCMS_ROOT = "lcms2" ,
378428 IMAGEQUANT_ROOT = "libimagequant" ,
379429 ).items ():
@@ -659,6 +709,39 @@ def build_extensions(self):
659709 if subdir :
660710 _add_directory (self .compiler .include_dirs , subdir , 0 )
661711
712+ if feature .freetype and feature .want ("raqm" ):
713+ if not feature .want_vendor ("raqm" ): # want system Raqm
714+ _dbg ("Looking for Raqm" )
715+ if _find_include_file (self , "raqm.h" ):
716+ if _find_library_file (self , "raqm" ):
717+ feature .raqm = "raqm"
718+ elif _find_library_file (self , "libraqm" ):
719+ feature .raqm = "libraqm"
720+ else : # want to build Raqm from src/thirdparty
721+ _dbg ("Looking for HarfBuzz" )
722+ feature .harfbuzz = None
723+ hb_dir = _find_include_dir (self , "harfbuzz" , "hb.h" )
724+ if hb_dir :
725+ if isinstance (hb_dir , str ):
726+ _add_directory (self .compiler .include_dirs , hb_dir , 0 )
727+ if _find_library_file (self , "harfbuzz" ):
728+ feature .harfbuzz = "harfbuzz"
729+ if feature .harfbuzz :
730+ if not feature .want_vendor ("fribidi" ): # want system FriBiDi
731+ _dbg ("Looking for FriBiDi" )
732+ feature .fribidi = None
733+ fribidi_dir = _find_include_dir (self , "fribidi" , "fribidi.h" )
734+ if fribidi_dir :
735+ if isinstance (fribidi_dir , str ):
736+ _add_directory (
737+ self .compiler .include_dirs , fribidi_dir , 0
738+ )
739+ if _find_library_file (self , "fribidi" ):
740+ feature .fribidi = "fribidi"
741+ feature .raqm = True
742+ else : # want to build FriBiDi shim from src/thirdparty
743+ feature .raqm = True
744+
662745 if feature .want ("lcms" ):
663746 _dbg ("Looking for lcms" )
664747 if _find_include_file (self , "lcms2.h" ):
@@ -754,9 +837,25 @@ def build_extensions(self):
754837 # additional libraries
755838
756839 if feature .freetype :
840+ srcs = []
757841 libs = ["freetype" ]
758842 defs = []
759- self ._update_extension ("PIL._imagingft" , libs , defs )
843+ if feature .raqm :
844+ if not feature .want_vendor ("raqm" ): # using system Raqm
845+ defs .append (("HAVE_RAQM" , None ))
846+ defs .append (("HAVE_RAQM_SYSTEM" , None ))
847+ libs .append (feature .raqm )
848+ else : # building Raqm from src/thirdparty
849+ defs .append (("HAVE_RAQM" , None ))
850+ srcs .append ("src/thirdparty/raqm/raqm.c" )
851+ libs .append (feature .harfbuzz )
852+ if not feature .want_vendor ("fribidi" ): # using system FriBiDi
853+ defs .append (("HAVE_FRIBIDI_SYSTEM" , None ))
854+ libs .append (feature .fribidi )
855+ else : # building FriBiDi shim from src/thirdparty
856+ srcs .append ("src/thirdparty/fribidi-shim/fribidi.c" )
857+ self ._update_extension ("PIL._imagingft" , libs , defs , srcs )
858+
760859 else :
761860 self ._remove_extension ("PIL._imagingft" )
762861
@@ -803,13 +902,20 @@ def summary_report(self, feature):
803902 print (f" [{ v .strip ()} " )
804903 print ("-" * 68 )
805904
905+ raqm_extra_info = ""
906+ if feature .want_vendor ("raqm" ):
907+ raqm_extra_info += "bundled"
908+ if feature .want_vendor ("fribidi" ):
909+ raqm_extra_info += ", FriBiDi shim"
910+
806911 options = [
807912 (feature .jpeg , "JPEG" ),
808913 (feature .jpeg2000 , "OPENJPEG (JPEG2000)" , feature .openjpeg_version ),
809914 (feature .zlib , "ZLIB (PNG/ZIP)" ),
810915 (feature .imagequant , "LIBIMAGEQUANT" ),
811916 (feature .tiff , "LIBTIFF" ),
812917 (feature .freetype , "FREETYPE2" ),
918+ (feature .raqm , "RAQM (Text shaping)" , raqm_extra_info ),
813919 (feature .lcms , "LITTLECMS2" ),
814920 (feature .webp , "WEBP" ),
815921 (feature .webpmux , "WEBPMUX" ),
@@ -819,10 +925,10 @@ def summary_report(self, feature):
819925 all = 1
820926 for option in options :
821927 if option [0 ]:
822- version = ""
928+ extra_info = ""
823929 if len (option ) >= 3 and option [2 ]:
824- version = f" ({ option [2 ]} )"
825- print (f"--- { option [1 ]} support available{ version } " )
930+ extra_info = f" ({ option [2 ]} )"
931+ print (f"--- { option [1 ]} support available{ extra_info } " )
826932 else :
827933 print (f"*** { option [1 ]} support not available" )
828934 all = 0
0 commit comments