more work in progress
git-svn-id: svn://svn.code.sf.net/p/fonttools/code/trunk@236 4cde692c-a291-49d1-8350-778aa11640f8
This commit is contained in:
parent
dc18128aa9
commit
1890b953f0
@ -1,7 +1,7 @@
|
||||
"""cffLib.py -- read/write tools for Adobe CFF fonts."""
|
||||
|
||||
#
|
||||
# $Id: cffLib.py,v 1.11 2002-05-14 13:51:51 jvr Exp $
|
||||
# $Id: cffLib.py,v 1.12 2002-05-15 07:41:30 jvr Exp $
|
||||
#
|
||||
|
||||
import struct, sstruct
|
||||
@ -31,13 +31,25 @@ class CFFFontSet:
|
||||
topDicts = readINDEX(file)
|
||||
strings = IndexedStrings(readINDEX(file))
|
||||
globalSubrs = readINDEX(file)
|
||||
|
||||
file.seek(4)
|
||||
xfnames = Index(file)
|
||||
xtopds = Index(file)
|
||||
xstrings = Index(file)
|
||||
xgsubrs = Index(file)
|
||||
|
||||
assert xfnames.toList() == self.fontNames
|
||||
assert xtopds.toList() == topDicts
|
||||
assert xstrings.toList() == strings.strings
|
||||
assert xgsubrs.toList() == globalSubrs
|
||||
|
||||
self.GlobalSubrs = map(psCharStrings.T2CharString, globalSubrs)
|
||||
|
||||
for i in range(len(topDicts)):
|
||||
font = self.fonts[self.fontNames[i]] = CFFFont()
|
||||
font.GlobalSubrs = self.GlobalSubrs
|
||||
file.seek(0)
|
||||
font.decompile(file, topDicts[i], strings, self) # maybe only 'on demand'?
|
||||
font.decompile(file, topDicts[i], strings, self)
|
||||
|
||||
def compile(self):
|
||||
strings = IndexedStrings()
|
||||
@ -57,7 +69,7 @@ class CFFFontSet:
|
||||
xmlWriter.newline()
|
||||
for i in range(len(self.GlobalSubrs)):
|
||||
xmlWriter.newline()
|
||||
xmlWriter.begintag("CharString", id=i)
|
||||
xmlWriter.begintag("CharString", index=i)
|
||||
xmlWriter.newline()
|
||||
self.GlobalSubrs[i].toXML(xmlWriter)
|
||||
xmlWriter.endtag("CharString")
|
||||
@ -81,19 +93,8 @@ class CFFFont:
|
||||
raise AttributeError, attr
|
||||
return topDictDefaults[attr]
|
||||
|
||||
def fromDict(self, dict):
|
||||
self.__dict__.update(dict)
|
||||
|
||||
def decompileCID(self, data, strings):
|
||||
offset = self.FDArray
|
||||
fontDicts, restdata = readINDEX(data[offset:])
|
||||
subFonts = []
|
||||
for topDictData in fontDicts:
|
||||
subFont = CFFFont()
|
||||
subFonts.append(subFont)
|
||||
subFont.decompile(data, topDictData, strings, None)
|
||||
|
||||
raise NotImplementedError
|
||||
def fromDict(self, d):
|
||||
self.__dict__.update(d)
|
||||
|
||||
def decompile(self, file, topDictData, strings, fontSet):
|
||||
top = TopDictDecompiler(strings)
|
||||
@ -124,29 +125,33 @@ class CFFFont:
|
||||
|
||||
if hasattr(self, "CharStrings"):
|
||||
file.seek(self.CharStrings)
|
||||
rawCharStrings = readINDEX(file)
|
||||
rawCharStrings = Index(file)
|
||||
nGlyphs = len(rawCharStrings)
|
||||
|
||||
# get charset (or rather: get glyphNames)
|
||||
if self.charset == 0:
|
||||
pass # XXX standard charset
|
||||
else:
|
||||
if self.charset > 2:
|
||||
file.seek(self.charset)
|
||||
format = ord(file.read(1))
|
||||
if format == 0:
|
||||
xxx
|
||||
raise NotImplementedError
|
||||
elif format == 1:
|
||||
charset = parseCharsetFormat1(nGlyphs, file, strings, isCID)
|
||||
elif format == 2:
|
||||
charset = parseCharsetFormat2(nGlyphs, file, strings, isCID)
|
||||
elif format == 3:
|
||||
xxx
|
||||
raise NotImplementedError
|
||||
else:
|
||||
xxx
|
||||
raise NotImplementedError
|
||||
self.charset = charset
|
||||
assert len(charset) == nGlyphs
|
||||
else:
|
||||
# self.charset:
|
||||
# 0: ISOAdobe (or CID font!)
|
||||
# 1: Expert
|
||||
# 2: ExpertSubset
|
||||
pass
|
||||
|
||||
if self.charset <> 0:
|
||||
if hasattr(self, "CharStrings"):
|
||||
self.CharStrings = charStrings = {}
|
||||
if self.CharstringType == 2:
|
||||
# Type 2 CharStrings
|
||||
@ -154,19 +159,20 @@ class CFFFont:
|
||||
else:
|
||||
# Type 1 CharStrings
|
||||
charStringClass = psCharStrings.T1CharString
|
||||
|
||||
for i in range(nGlyphs):
|
||||
charStrings[charset[i]] = charStringClass(rawCharStrings[i])
|
||||
assert len(charStrings) == nGlyphs
|
||||
else:
|
||||
assert not hasattr(self, "CharStrings")
|
||||
|
||||
# XXX Encoding!
|
||||
encoding = self.Encoding
|
||||
if encoding not in (0, 1):
|
||||
# encoding is an _offset_ from the beginning of 'data' to an encoding subtable
|
||||
XXX
|
||||
if encoding > 1:
|
||||
# encoding is an offset from the beginning of 'data' to an encoding subtable
|
||||
raise NotImplementedError
|
||||
self.Encoding = encoding
|
||||
else:
|
||||
# self.Encoding:
|
||||
# 0 Standard Encoding
|
||||
# 1 Expert Encoding
|
||||
pass
|
||||
|
||||
def getGlyphOrder(self):
|
||||
return self.charset
|
||||
@ -189,78 +195,7 @@ class CFFFont:
|
||||
|
||||
def toXML(self, xmlWriter, progress=None):
|
||||
xmlWriter.newline()
|
||||
# first dump the simple values
|
||||
self.toXMLSimpleValues(xmlWriter)
|
||||
|
||||
# dump charset
|
||||
# XXX
|
||||
|
||||
# decompile all charstrings
|
||||
if progress:
|
||||
progress.setlabel("Decompiling CharStrings...")
|
||||
self.decompileAllCharStrings()
|
||||
|
||||
# dump private dict
|
||||
xmlWriter.begintag("Private")
|
||||
xmlWriter.newline()
|
||||
self.Private.toXML(xmlWriter)
|
||||
xmlWriter.endtag("Private")
|
||||
xmlWriter.newline()
|
||||
|
||||
self.toXMLCharStrings(xmlWriter, progress)
|
||||
|
||||
def toXMLSimpleValues(self, xmlWriter):
|
||||
keys = self.__dict__.keys()
|
||||
keys.remove("CharStrings")
|
||||
keys.remove("Private")
|
||||
keys.remove("charset")
|
||||
keys.remove("GlobalSubrs")
|
||||
keys.sort()
|
||||
for key in keys:
|
||||
value = getattr(self, key)
|
||||
if key == "Encoding":
|
||||
if value == 0:
|
||||
# encoding is (Adobe) Standard Encoding
|
||||
value = "StandardEncoding"
|
||||
elif value == 1:
|
||||
# encoding is Expert Encoding
|
||||
value = "ExpertEncoding"
|
||||
if type(value) == types.ListType:
|
||||
value = string.join(map(str, value), " ")
|
||||
else:
|
||||
value = str(value)
|
||||
xmlWriter.begintag(key)
|
||||
if hasattr(value, "toXML"):
|
||||
xmlWriter.newline()
|
||||
value.toXML(xmlWriter)
|
||||
xmlWriter.newline()
|
||||
else:
|
||||
xmlWriter.write(value)
|
||||
xmlWriter.endtag(key)
|
||||
xmlWriter.newline()
|
||||
xmlWriter.newline()
|
||||
|
||||
def toXMLCharStrings(self, xmlWriter, progress=None):
|
||||
charStrings = self.CharStrings
|
||||
xmlWriter.newline()
|
||||
xmlWriter.begintag("CharStrings")
|
||||
xmlWriter.newline()
|
||||
glyphNames = charStrings.keys()
|
||||
glyphNames.sort()
|
||||
for glyphName in glyphNames:
|
||||
if progress:
|
||||
progress.setlabel("Dumping 'CFF ' table... (%s)" % glyphName)
|
||||
progress.increment()
|
||||
xmlWriter.newline()
|
||||
charString = charStrings[glyphName]
|
||||
xmlWriter.begintag("CharString", name=glyphName)
|
||||
xmlWriter.newline()
|
||||
charString.toXML(xmlWriter)
|
||||
xmlWriter.endtag("CharString")
|
||||
xmlWriter.newline()
|
||||
xmlWriter.newline()
|
||||
xmlWriter.endtag("CharStrings")
|
||||
xmlWriter.newline()
|
||||
genericToXML(self, topDictOrder, {'CharStrings': 'CharString'}, xmlWriter)
|
||||
|
||||
|
||||
class PrivateDict:
|
||||
@ -276,48 +211,22 @@ class PrivateDict:
|
||||
# get local subrs
|
||||
if hasattr(self, "Subrs"):
|
||||
file.seek(self.Subrs, 1)
|
||||
localSubrs = readINDEX(file)
|
||||
localSubrs = Index(file)
|
||||
self.Subrs = map(psCharStrings.T2CharString, localSubrs)
|
||||
else:
|
||||
self.Subrs = []
|
||||
|
||||
def toXML(self, xmlWriter):
|
||||
xmlWriter.newline()
|
||||
keys = self.__dict__.keys()
|
||||
keys.remove("Subrs")
|
||||
for key in keys:
|
||||
value = getattr(self, key)
|
||||
if type(value) == types.ListType:
|
||||
value = string.join(map(str, value), " ")
|
||||
else:
|
||||
value = str(value)
|
||||
xmlWriter.begintag(key)
|
||||
xmlWriter.write(value)
|
||||
xmlWriter.endtag(key)
|
||||
xmlWriter.newline()
|
||||
# write subroutines
|
||||
xmlWriter.newline()
|
||||
xmlWriter.begintag("Subrs")
|
||||
xmlWriter.newline()
|
||||
for i in range(len(self.Subrs)):
|
||||
xmlWriter.newline()
|
||||
xmlWriter.begintag("CharString", id=i)
|
||||
xmlWriter.newline()
|
||||
self.Subrs[i].toXML(xmlWriter)
|
||||
xmlWriter.endtag("CharString")
|
||||
xmlWriter.newline()
|
||||
xmlWriter.newline()
|
||||
xmlWriter.endtag("Subrs")
|
||||
xmlWriter.newline()
|
||||
xmlWriter.newline()
|
||||
genericToXML(self, privateDictOrder, {'Subrs': 'CharString'}, xmlWriter)
|
||||
|
||||
def __getattr__(self, attr):
|
||||
if not privateDictDefaults.has_key(attr):
|
||||
raise AttributeError, attr
|
||||
return privateDictDefaults[attr]
|
||||
|
||||
def fromDict(self, dict):
|
||||
self.__dict__.update(dict)
|
||||
def fromDict(self, d):
|
||||
self.__dict__.update(d)
|
||||
|
||||
|
||||
def readINDEX(file):
|
||||
@ -344,6 +253,111 @@ def readINDEX(file):
|
||||
return stuff
|
||||
|
||||
|
||||
class Index:
|
||||
|
||||
def __init__(self, file):
|
||||
self.file = file
|
||||
count, = struct.unpack(">H", file.read(2))
|
||||
self.count = count
|
||||
if count == 0:
|
||||
self.offsets = []
|
||||
return
|
||||
offSize = ord(file.read(1))
|
||||
self.offsets = offsets = []
|
||||
pad = '\0' * (4 - offSize)
|
||||
for index in range(count+1):
|
||||
chunk = file.read(offSize)
|
||||
chunk = pad + chunk
|
||||
offset, = struct.unpack(">L", chunk)
|
||||
offsets.append(int(offset))
|
||||
self.offsetBase = file.tell() - 1
|
||||
file.seek(self.offsetBase + offsets[-1])
|
||||
|
||||
def __len__(self):
|
||||
return self.count
|
||||
|
||||
def __getitem__(self, index):
|
||||
offset = self.offsets[index] + self.offsetBase
|
||||
size = self.offsets[index+1] - self.offsets[index]
|
||||
return FileString(self.file, offset, size)
|
||||
|
||||
def toList(self):
|
||||
l = []
|
||||
for item in self:
|
||||
l.append(item[:])
|
||||
return l
|
||||
|
||||
|
||||
class FileString:
|
||||
|
||||
def __init__(self, file, offset, size):
|
||||
self.file = file
|
||||
self.offset = offset
|
||||
self.size = size
|
||||
self.string = None
|
||||
|
||||
def __len__(self):
|
||||
return self.size
|
||||
|
||||
def __getitem__(self, index):
|
||||
return self.get()[index]
|
||||
|
||||
def __getslice__(self, i, j):
|
||||
return self.get()[i:j]
|
||||
|
||||
def get(self):
|
||||
if self.string is None:
|
||||
self.file.seek(self.offset)
|
||||
self.string = self.file.read(self.size)
|
||||
return self.string
|
||||
|
||||
|
||||
def getItems(o):
|
||||
if hasattr(o, "items"):
|
||||
items = o.items()
|
||||
items.sort()
|
||||
return "name", items
|
||||
else:
|
||||
return "index", map(None, range(len(o)), o)
|
||||
|
||||
|
||||
def genericToXML(obj, order, arrayTypes, xmlWriter):
|
||||
for name in order:
|
||||
value = getattr(obj, name, None)
|
||||
if value is None:
|
||||
continue
|
||||
if hasattr(value, "toXML"):
|
||||
xmlWriter.newline()
|
||||
xmlWriter.begintag(name)
|
||||
xmlWriter.newline()
|
||||
value.toXML(xmlWriter)
|
||||
xmlWriter.endtag(name)
|
||||
xmlWriter.newline()
|
||||
xmlWriter.newline()
|
||||
elif arrayTypes.has_key(name):
|
||||
typeName = arrayTypes[name]
|
||||
xmlWriter.newline()
|
||||
xmlWriter.begintag(name)
|
||||
xmlWriter.newline()
|
||||
xmlWriter.newline()
|
||||
label, items = getItems(value)
|
||||
for k, v in items:
|
||||
xmlWriter.begintag(typeName, [(label, k)])
|
||||
xmlWriter.newline()
|
||||
v.toXML(xmlWriter)
|
||||
xmlWriter.endtag(typeName)
|
||||
xmlWriter.newline()
|
||||
xmlWriter.newline()
|
||||
xmlWriter.endtag(name)
|
||||
xmlWriter.newline()
|
||||
xmlWriter.newline()
|
||||
else:
|
||||
if isinstance(value, types.ListType):
|
||||
value = " ".join(map(str, value))
|
||||
xmlWriter.simpletag(name, value=value)
|
||||
xmlWriter.newline()
|
||||
|
||||
|
||||
def parseCharsetFormat1(nGlyphs, file, strings, isCID):
|
||||
charset = ['.notdef']
|
||||
count = 1
|
||||
@ -376,105 +390,101 @@ def parseCharsetFormat2(nGlyphs, file, strings, isCID):
|
||||
return charset
|
||||
|
||||
|
||||
def buildOperatorDict(table):
|
||||
d = {}
|
||||
for row in table:
|
||||
d[row[0]] = row[1:3]
|
||||
return d
|
||||
|
||||
def buildOrder(table):
|
||||
l = []
|
||||
for row in table:
|
||||
l.append(row[1])
|
||||
return l
|
||||
|
||||
def buildDefaults(table):
|
||||
d = {}
|
||||
for row in table:
|
||||
if row[3] is not None:
|
||||
d[row[1]] = row[3]
|
||||
return d
|
||||
|
||||
|
||||
topDictOperators = [
|
||||
# opcode name argument type
|
||||
(0, 'version', 'SID'),
|
||||
(1, 'Notice', 'SID'),
|
||||
(2, 'FullName', 'SID'),
|
||||
(3, 'FamilyName', 'SID'),
|
||||
(4, 'Weight', 'SID'),
|
||||
(5, 'FontBBox', 'array'),
|
||||
(13, 'UniqueID', 'number'),
|
||||
(14, 'XUID', 'array'),
|
||||
(15, 'charset', 'number'),
|
||||
(16, 'Encoding', 'number'),
|
||||
(17, 'CharStrings', 'number'),
|
||||
(18, 'Private', ('number', 'number')),
|
||||
((12, 0), 'Copyright', 'SID'),
|
||||
((12, 1), 'isFixedPitch', 'number'),
|
||||
((12, 2), 'ItalicAngle', 'number'),
|
||||
((12, 3), 'UnderlinePosition', 'number'),
|
||||
((12, 4), 'UnderlineThickness', 'number'),
|
||||
((12, 5), 'PaintType', 'number'),
|
||||
((12, 6), 'CharstringType', 'number'),
|
||||
((12, 7), 'FontMatrix', 'array'),
|
||||
((12, 8), 'StrokeWidth', 'number'),
|
||||
((12, 20), 'SyntheticBase', 'number'),
|
||||
((12, 21), 'PostScript', 'SID'),
|
||||
((12, 22), 'BaseFontName', 'SID'),
|
||||
# CID additions
|
||||
((12, 30), 'ROS', ('SID', 'SID', 'number')),
|
||||
((12, 31), 'CIDFontVersion', 'number'),
|
||||
((12, 32), 'CIDFontRevision', 'number'),
|
||||
((12, 33), 'CIDFontType', 'number'),
|
||||
((12, 34), 'CIDCount', 'number'),
|
||||
((12, 35), 'UIDBase', 'number'),
|
||||
((12, 36), 'FDArray', 'number'),
|
||||
((12, 37), 'FDSelect', 'number'),
|
||||
((12, 38), 'FontName', 'SID'),
|
||||
# opcode name argument type default
|
||||
(0, 'version', 'SID', None),
|
||||
(1, 'Notice', 'SID', None),
|
||||
((12, 0), 'Copyright', 'SID', None),
|
||||
(2, 'FullName', 'SID', None),
|
||||
(3, 'FamilyName', 'SID', None),
|
||||
(4, 'Weight', 'SID', None),
|
||||
((12, 1), 'isFixedPitch', 'number', 0),
|
||||
((12, 2), 'ItalicAngle', 'number', 0),
|
||||
((12, 3), 'UnderlinePosition', 'number', None),
|
||||
((12, 4), 'UnderlineThickness', 'number', 50),
|
||||
((12, 5), 'PaintType', 'number', 0),
|
||||
((12, 6), 'CharstringType', 'number', 2),
|
||||
((12, 7), 'FontMatrix', 'array', [0.001,0,0,0.001,0,0]),
|
||||
(13, 'UniqueID', 'number', None),
|
||||
(5, 'FontBBox', 'array', [0,0,0,0]),
|
||||
((12, 8), 'StrokeWidth', 'number', 0),
|
||||
(14, 'XUID', 'array', None),
|
||||
(15, 'charset', 'number', 0),
|
||||
(16, 'Encoding', 'number', 0),
|
||||
(18, 'Private', ('number','number'), None),
|
||||
(17, 'CharStrings', 'number', None),
|
||||
((12, 20), 'SyntheticBase', 'number', None),
|
||||
((12, 21), 'PostScript', 'SID', None),
|
||||
((12, 22), 'BaseFontName', 'SID', None),
|
||||
((12, 23), 'BaseFontBlend', 'delta', None),
|
||||
((12, 30), 'ROS', ('SID','SID','number'), None),
|
||||
((12, 31), 'CIDFontVersion', 'number', 0),
|
||||
((12, 32), 'CIDFontRevision', 'number', 0),
|
||||
((12, 33), 'CIDFontType', 'number', 0),
|
||||
((12, 34), 'CIDCount', 'number', 8720),
|
||||
((12, 35), 'UIDBase', 'number', None),
|
||||
((12, 36), 'FDArray', 'number', None),
|
||||
((12, 37), 'FDSelect', 'number', None),
|
||||
((12, 38), 'FontName', 'SID', None),
|
||||
]
|
||||
|
||||
topDictDefaults = {
|
||||
'isFixedPitch': 0,
|
||||
'ItalicAngle': 0,
|
||||
'UnderlineThickness': 50,
|
||||
'PaintType': 0,
|
||||
'CharstringType': 2,
|
||||
'FontMatrix': [0.001, 0, 0, 0.001, 0, 0],
|
||||
'FontBBox': [0, 0, 0, 0],
|
||||
'StrokeWidth': 0,
|
||||
'charset': 0,
|
||||
'Encoding': 0,
|
||||
# CID defaults
|
||||
'CIDFontVersion': 0,
|
||||
'CIDFontRevision': 0,
|
||||
'CIDFontType': 0,
|
||||
'CIDCount': 8720,
|
||||
}
|
||||
topDictDefaults = buildDefaults(topDictOperators)
|
||||
topDictOrder = buildOrder(topDictOperators)
|
||||
|
||||
class TopDictDecompiler(psCharStrings.DictDecompiler):
|
||||
|
||||
operators = psCharStrings.buildOperatorDict(topDictOperators)
|
||||
operators = buildOperatorDict(topDictOperators)
|
||||
dictDefaults = topDictDefaults
|
||||
|
||||
|
||||
privateDictOperators = [
|
||||
# opcode name argument type
|
||||
(6, 'BlueValues', 'array'),
|
||||
(7, 'OtherBlues', 'array'),
|
||||
(8, 'FamilyBlues', 'array'),
|
||||
(9, 'FamilyOtherBlues', 'array'),
|
||||
(10, 'StdHW', 'number'),
|
||||
(11, 'StdVW', 'number'),
|
||||
(19, 'Subrs', 'number'),
|
||||
(20, 'defaultWidthX', 'number'),
|
||||
(21, 'nominalWidthX', 'number'),
|
||||
((12, 9), 'BlueScale', 'number'),
|
||||
((12, 10), 'BlueShift', 'number'),
|
||||
((12, 11), 'BlueFuzz', 'number'),
|
||||
((12, 12), 'StemSnapH', 'array'),
|
||||
((12, 13), 'StemSnapV', 'array'),
|
||||
((12, 14), 'ForceBold', 'number'),
|
||||
((12, 17), 'LanguageGroup', 'number'),
|
||||
((12, 18), 'ExpansionFactor', 'number'),
|
||||
((12, 19), 'initialRandomSeed', 'number'),
|
||||
# opcode name argument type default
|
||||
(6, 'BlueValues', 'delta', None),
|
||||
(7, 'OtherBlues', 'delta', None),
|
||||
(8, 'FamilyBlues', 'delta', None),
|
||||
(9, 'FamilyOtherBlues', 'delta', None),
|
||||
((12, 9), 'BlueScale', 'number', 0.039625),
|
||||
((12, 10), 'BlueShift', 'number', 7),
|
||||
((12, 11), 'BlueFuzz', 'number', 1),
|
||||
(10, 'StdHW', 'number', None),
|
||||
(11, 'StdVW', 'number', None),
|
||||
((12, 12), 'StemSnapH', 'delta', None),
|
||||
((12, 13), 'StemSnapV', 'delta', None),
|
||||
((12, 14), 'ForceBold', 'number', 0),
|
||||
((12, 17), 'LanguageGroup', 'number', 0),
|
||||
((12, 18), 'ExpansionFactor', 'number', 0.06),
|
||||
((12, 19), 'initialRandomSeed', 'number', 0),
|
||||
(20, 'defaultWidthX', 'number', 0),
|
||||
(21, 'nominalWidthX', 'number', 0),
|
||||
(19, 'Subrs', 'number', None),
|
||||
]
|
||||
|
||||
privateDictDefaults = {
|
||||
'defaultWidthX': 0,
|
||||
'nominalWidthX': 0,
|
||||
'BlueScale': 0.039625,
|
||||
'BlueShift': 7,
|
||||
'BlueFuzz': 1,
|
||||
'ForceBold': 0,
|
||||
'LanguageGroup': 0,
|
||||
'ExpansionFactor': 0.06,
|
||||
'initialRandomSeed': 0,
|
||||
}
|
||||
privateDictDefaults = buildDefaults(privateDictOperators)
|
||||
privateDictOrder = buildOrder(privateDictOperators)
|
||||
|
||||
class PrivateDictDecompiler(psCharStrings.DictDecompiler):
|
||||
|
||||
operators = psCharStrings.buildOperatorDict(privateDictOperators)
|
||||
operators = buildOperatorDict(privateDictOperators)
|
||||
dictDefaults = privateDictDefaults
|
||||
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user