Initial commit of the TextureCreator

This commit is contained in:
Ochi Wolfe 2013-08-25 23:06:03 +02:00
parent a5e2744fc0
commit 707d234e2b
39 changed files with 1547 additions and 0 deletions

View file

@ -0,0 +1,89 @@
'''
This script is part of the FURWARE texture creator, a texture
generation script for the FURWARE text text-on-prim script
for Second Life(R).
Please see the included README and LICENSE files in the root
directory for more information.
'''
'''
This script loads a font file using the FreeType library
for usage with (py)cairo. This is merely a workaround because
the python binding to cairo currently doesn't support loading
of fonts from a file. The function is based on the code from
http://www.cairographics.org/freetypepython/
'''
import cairo
import ctypes
import ctypes.util
_freetypeInitialized = False
class PycairoContext(ctypes.Structure):
_fields_ = [
("PyObject_HEAD", ctypes.c_byte * object.__basicsize__),
("ctx", ctypes.c_void_p),
("base", ctypes.c_void_p)
]
def fontFaceFromFile(filename):
global _freetypeInitialized
global _freetype_so
global _cairo_so
global _ft_lib
global _surface
CAIRO_STATUS_SUCCESS = 0
FT_Err_Ok = 0
if not _freetypeInitialized:
# Find shared libraries.
freetypeLibName = ctypes.util.find_library("freetype")
if not freetypeLibName:
raise Exception("FreeType library not found.")
cairoLibName = ctypes.util.find_library("cairo")
if not cairoLibName:
raise Exception("Cairo library not found.")
_freetype_so = ctypes.CDLL(freetypeLibName)
_cairo_so = ctypes.CDLL(cairoLibName)
_cairo_so.cairo_ft_font_face_create_for_ft_face.restype = ctypes.c_void_p
_cairo_so.cairo_ft_font_face_create_for_ft_face.argtypes = [ctypes.c_void_p, ctypes.c_int]
_cairo_so.cairo_set_font_face.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
_cairo_so.cairo_font_face_status.argtypes = [ctypes.c_void_p]
_cairo_so.cairo_status.argtypes = [ctypes.c_void_p]
# Initialize FreeType.
_ft_lib = ctypes.c_void_p()
if _freetype_so.FT_Init_FreeType(ctypes.byref(_ft_lib)) != FT_Err_Ok:
raise Exception("Error initialising FreeType library.")
_surface = cairo.ImageSurface(cairo.FORMAT_A8, 0, 0)
_freetypeInitialized = True
# Create FreeType face.
ftFace = ctypes.c_void_p()
cairo_ctx = cairo.Context(_surface)
cairo_t = PycairoContext.from_address(id(cairo_ctx)).ctx
if _freetype_so.FT_New_Face(_ft_lib, filename.encode("utf-8"), 0, ctypes.byref(ftFace)) != FT_Err_Ok:
raise Exception("Error creating FreeType font face for " + filename)
# Create Cairo font face for FreeType face.
cr_face = _cairo_so.cairo_ft_font_face_create_for_ft_face(ftFace, 0)
if CAIRO_STATUS_SUCCESS != _cairo_so.cairo_font_face_status(cr_face):
raise Exception("Error creating cairo font face for " + filename)
_cairo_so.cairo_set_font_face(cairo_t, cr_face)
if CAIRO_STATUS_SUCCESS != _cairo_so.cairo_status(cairo_t):
raise Exception("Error creating cairo font face for " + filename)
face = cairo_ctx.get_font_face()
return face

View file

@ -0,0 +1,57 @@
'''
This script is part of the FURWARE texture creator, a texture
generation script for the FURWARE text text-on-prim script
for Second Life(R).
Please see the included README and LICENSE files in the root
directory for more information.
'''
'''
This script is a wrapper for loading script and config files.
'''
import codecs
import re
class ScriptReader:
def __init__(self, scriptPath):
scriptFile = codecs.open(scriptPath, "r", "utf-8")
self.tokenList = []
self.rawArgList = []
for line in scriptFile:
if line[0] == "#":
continue
cleanedLine = line.rstrip("\n")
tokens = cleanedLine.split()
if len(tokens):
self.tokenList.append(tokens)
self.rawArgList.append(cleanedLine[len(tokens[0])+1:])
scriptFile.close()
self.curLine = 0
# Shall return True iff there is at least one more line
# to parse the the moment this function is called.
def more(self):
if self.curLine >= len(self.tokenList):
return False
self.tokens = self.tokenList[self.curLine]
self.rawArg = self.rawArgList[self.curLine]
self.curLine += 1
return True
def getCmd(self):
return self.tokens[0]
def getArgs(self, specs):
if len(specs)+1 != len(self.tokens):
raise Exception("Invalid argument list.")
result = []
for i in range(0, len(specs)):
result.append(specs[i](self.tokens[i+1]))
return result
def getRawArg(self):
return self.rawArg

View file

@ -0,0 +1,33 @@
'''
This script is part of the FURWARE texture creator, a texture
generation script for the FURWARE text text-on-prim script
for Second Life(R).
Please see the included README and LICENSE files in the root
directory for more information.
'''
import struct, array
def writeTGA(width, height, bitsPerColor, bitsPerPixel, data, path):
FORMAT = "<BBBHHBHHHHBB"
header = struct.pack(FORMAT,
0, # Offset
0, # ColorType
3, # ImageType
0, # PaletteStart
0, # PaletteLen
bitsPerColor, # PalBits
0, # XOrigin
0, # YOrigin
width, # Width
height, # Height
bitsPerPixel, # BPP
32 # Orientation
)
file = open(path, "wb")
file.write(header)
file.write(data)
file.close()

View file

@ -0,0 +1,326 @@
'''
This script is part of the FURWARE texture creator, a texture
generation script for the FURWARE text text-on-prim script
for Second Life(R).
Please see the included README and LICENSE files in the root
directory for more information.
'''
'''
This script is the heart of the texture creator. It creates a
texture, executes script files to perform actions on it and
writes the result out to an image file.
'''
import cairo
import itertools
import math
import os
from . import FontFaceCreator
from . import TGAWriter
from ScriptReader import ScriptReader
OUTPUT_DIR = "output"
SCRIPTS_DIR = "scripts"
class GridConfig:
def __init__(self, args):
self.imageSize = (args[0], args[1])
self.cellSize = (32, 64)
self.begin = (32, 44)
self.spacing = (40, 72)
self.cellCount = (25, 14)
class CharConfig:
def __init__(self, args):
self.offsetX = args[0]
self.offsetY = args[1]
self.scaleX = args[2]
self.scaleY = args[3]
class FontConfig:
def __init__(self, fontConfPath):
self.offsetX = 0
self.offsetY = 0
self.offsetXUnit = ""
self.offsetYUnit = ""
self.perCharConfigs = {}
self.guessingChar = "X"
self.scale = 0.75
self.scaleUnit = "cellHeight"
self.autoShrink = False
if not os.path.exists(fontConfPath):
return
fontConf = ScriptReader(fontConfPath)
while fontConf.more():
if fontConf.getCmd() == "offset":
[self.offsetX, self.offsetY, self.offsetXUnit, self.offsetYUnit] = fontConf.getArgs([int, int, str, str])
elif fontConf.getCmd() == "scale":
[self.scale, self.scaleUnit] = fontConf.getArgs([float, str])
elif fontConf.getCmd() == "guessingChar":
self.guessingChar = fontConf.getArgs([unicode])[0]
elif fontConf.getCmd() == "enableAutoShrink":
self.autoShrink = True
elif fontConf.getCmd() == "charOffset":
args = fontConf.getArgs([unicode, int, int, float, float])
self.perCharConfigs[args[0]] = CharConfig(args[1:])
def getCharConfig(self, char):
try:
return self.perCharConfigs[char]
except KeyError:
return CharConfig([0, 0, 1.0, 1.0])
class FontExtents:
def __init__(self, cairoContext):
self.cairoContext = cairoContext
def update(self, text):
self.xBearing, self.yBearing, self.xSize, self.ySize, self.xAdvance, self.yAdvance \
= self.cairoContext.text_extents(text)
class TexturePainter:
def __init__(self, outputName):
self.debugGrid = False
self.outputName = outputName
self.defaultOutputName = outputName
def runScript(self, scriptName, defaultFont):
scriptPath = os.path.join(os.path.dirname(defaultFont), scriptName)
if not os.path.exists(scriptPath):
# Then try the default scripts directory.
scriptPath = os.path.join(SCRIPTS_DIR, scriptName)
if not os.path.exists(scriptPath):
print(" WARNING: Script \"" + scriptName + "\" not found.")
return False
script = ScriptReader(scriptPath)
while script.more():
if script.getCmd() == "runScript":
if not self.runScript(script.getRawArg(), defaultFont):
return False
elif script.getCmd() == "noBuild":
return False
elif script.getCmd() == "setOutputName":
self.outputName = script.getRawArg()
elif script.getCmd() == "setOutputNameSuffix":
self.outputName = self.defaultOutputName + script.getRawArg()
elif script.getCmd() == "init":
self.init(script.getArgs([int, int, float, float]))
elif script.getCmd() == "setCellSize":
self.gridConfig.cellSize = script.getArgs([int, int])
elif script.getCmd() == "setCellOffset":
self.gridConfig.begin = script.getArgs([int, int])
elif script.getCmd() == "setCellSpacing":
self.gridConfig.spacing = script.getArgs([int, int])
elif script.getCmd() == "setCellCount":
self.gridConfig.cellCount = script.getArgs([int, int])
elif script.getCmd() == "drawLineBetweenCells":
self.lineBetweenCells(*[int(arg) for arg in script.getArgs([int, int, int, int, int, int, int, int, int])])
elif script.getCmd() == "drawRectBetweenCells":
self.rectangleBetweenCells(*[int(arg) for arg in script.getArgs([int, int, int, int, int, int, int, int, int])])
elif script.getCmd() == "drawFilledRectBetweenCells":
self.rectangleBetweenCells(*[int(arg) for arg in script.getArgs([int, int, int, int, int, int, int, int, int])], filled = True)
elif script.getCmd() == "drawChars":
for char in script.getRawArg():
self.drawChar(char)
elif script.getCmd() == "loadFont":
fontPath = script.getRawArg()
if fontPath == "":
fontPath = defaultFont
self.loadFont(fontPath)
elif script.getCmd() == "jumpToCell":
self.jumpToCell(*script.getArgs([int, int]))
elif script.getCmd() == "drawDebugGrid":
self.debugGrid = True
self.cairoContext.save()
self.cairoContext.set_source_rgb(0,0,0)
self.cairoContext.set_matrix(cairo.Matrix(
xx = self.gridConfig.imageSize[0],
yy = self.gridConfig.imageSize[1]
))
self.cairoContext.rectangle(0,0,1,1)
self.cairoContext.fill()
self.cairoContext.restore()
self.cairoContext.save()
self.cairoContext.set_source_rgb(0.5,0.5,0.5)
thickness = 2
for y in range(0, self.gridConfig.cellCount[1]):
for x in range(0, self.gridConfig.cellCount[0]):
dx = self.gridConfig.cellSize[0]/2 + thickness/2
dy = self.gridConfig.cellSize[1]/2 + thickness/2
self.rectangleBetweenCells(thickness, x, y, x, y, -dx, -dy, dx, dy)
self.cairoContext.restore()
else:
print(" ERROR: Unknown script command \"" + script.getCmd() + "\".")
return False
return True
def writeTexture(self):
self.cairoSurface.flush()
buf = self.cairoSurface.get_data()
arr = bytearray(buf)
del arr[0::4]
del arr[0::3]
if not self.debugGrid:
arr[0::2] = itertools.repeat(255, self.gridConfig.imageSize[0] * self.gridConfig.imageSize[1])
targetPath = os.path.join(OUTPUT_DIR, self.outputName + ".tga")
TGAWriter.writeTGA(self.gridConfig.imageSize[0], self.gridConfig.imageSize[1], 8, 16, arr, targetPath)
def init(self, args):
imageSizeX = int(math.floor(args[2] * args[0]))
imageSizeY = int(math.floor(args[3] * args[1]))
self.gridConfig = GridConfig([imageSizeX, imageSizeY])
self.cairoSurface = cairo.ImageSurface(cairo.FORMAT_ARGB32, imageSizeX, imageSizeY)
self.cairoContext = cairo.Context(self.cairoSurface)
self.cairoContext.set_matrix(cairo.Matrix(
xx = args[2], yy = args[3]
))
self.cairoContext.set_source_rgb(1,1,1)
fontOptions = cairo.FontOptions()
fontOptions.set_antialias(cairo.ANTIALIAS_GRAY)
fontOptions.set_hint_style(cairo.HINT_STYLE_NONE)
self.cairoContext.set_font_options(fontOptions)
self.currentRow = 0
self.currentCol = 0
def loadFont(self, fontPath):
self.fontConfig = FontConfig(os.path.join(os.path.dirname(fontPath), "font.conf"))
fontFace = FontFaceCreator.fontFaceFromFile(fontPath)
self.cairoContext.set_font_face(fontFace)
self.fontSize = self.fontConfig.scale * self.convertScaleUnit(self.fontConfig.scaleUnit)
self.cairoContext.set_font_size(self.fontSize)
self.fontExtents = FontExtents(self.cairoContext)
self.fontExtents.update(self.fontConfig.guessingChar)
self.guessedYOffset = self.fontExtents.ySize / 2
def jumpToCell(self, x, y):
self.currentCol = int(x)
self.currentRow = int(y)
def drawChar(self, char):
fontSizeX = self.fontSize
fontSizeY = self.fontSize
if self.fontConfig.autoShrink:
self.cairoContext.set_font_size(1)
self.fontExtents.update(char)
if self.fontExtents.xSize > 0:
widthGuess = self.gridConfig.cellSize[0] / self.fontExtents.xSize
if widthGuess < self.fontSize:
fontSizeX = widthGuess
charConfig = self.fontConfig.getCharConfig(char)
fontSizeX *= charConfig.scaleX
fontSizeY *= charConfig.scaleY
self.cairoContext.set_font_matrix(cairo.Matrix(xx = fontSizeX, yy = fontSizeY))
self.fontExtents.update(char)
xOffset = -self.fontExtents.xAdvance / 2
yOffset = self.guessedYOffset
xOffset += self.fontConfig.offsetX * self.convertScaleUnit(self.fontConfig.offsetXUnit)
yOffset += self.fontConfig.offsetY * self.convertScaleUnit(self.fontConfig.offsetYUnit)
xOffset += charConfig.offsetX
yOffset += charConfig.offsetY
x = self.gridConfig.begin[0] + self.currentCol * self.gridConfig.spacing[0] + xOffset
y = self.gridConfig.begin[1] + self.currentRow * self.gridConfig.spacing[1] + yOffset
self.cairoContext.move_to(x, y)
self.cairoContext.show_text(char)
self.currentCol += 1
if self.currentCol >= self.gridConfig.cellCount[0]:
self.currentRow += 1
self.currentCol = 0
def cellPos(self, x, y):
return (
self.gridConfig.begin[0] + x * self.gridConfig.spacing[0],
self.gridConfig.begin[1] + y * self.gridConfig.spacing[1]
)
def lineBetweenCells(self, lineWidth, x0, y0, x1, y1, dx0 = 0, dy0 = 0, dx1 = 0, dy1 = 0):
self.cairoContext.set_line_width(lineWidth)
beg = self.cellPos(x0, y0)
end = self.cellPos(x1, y1)
begD = (dx0, dy0)
endD = (dx1, dy1)
self.cairoContext.move_to(beg[0]+begD[0], beg[1]+begD[1])
self.cairoContext.line_to(end[0]+endD[0], end[1]+endD[1])
self.cairoContext.stroke()
def rectangleBetweenCells(self, lineWidth, begX, begY, endX, endY, \
begDX = 0, begDY = 0, endDX = 0, endDY = 0, filled = False):
self.cairoContext.set_line_width(lineWidth)
beg = self.cellPos(begX, begY)
end = self.cellPos(endX, endY)
begD = (begDX, begDY)
endD = (endDX, endDY)
self.cairoContext.rectangle(
beg[0]+begD[0],
beg[1]+begD[1],
end[0]-beg[0]-begD[0]+endD[0],
end[1]-beg[1]-begD[1]+endD[1]
)
if filled:
self.cairoContext.fill()
else:
self.cairoContext.stroke()
def convertScaleUnit(self, unitString):
if unitString == "cellWidth":
return self.gridConfig.cellSize[0]
elif unitString == "cellHeight":
return self.gridConfig.cellSize[1]
return 1

View file

@ -0,0 +1,12 @@
'''
This script is part of the FURWARE texture creator, a texture
generation script for the FURWARE text text-on-prim script
for Second Life(R).
Please see the included README and LICENSE files in the root
directory for more information.
'''
'''
This script exists to qualify this directory as a python module.
'''