font (StaticFont) from image?

Yep, I enabled transparency.

BTW, do you see anything wrong with the UVs? It seems wrong when rendered.

Transparency works fine for me when I added that line to your file. Here’s my copy of your loadbin.py:

import struct
import math # square root

import Image # PIL

from pandac.PandaModules import *
import direct.directbase.DirectStart

class FontLoader:
    def __init__(self):
        self.charmap = {}
        lines = open('jascii2unicode.txt').readlines()
        for i in lines:
            list = i.split('\t') # \t = tab
            self.charmap[str(int(list[0], 16))] = str(int(list[1], 16))
        
        format = GeomVertexFormat.getV3t2() # Panda3D pre-defines a handful of standard GeomVertexFormat. V3t2 supports vertices and UVs (all that we need)
    
        self.vdata = GeomVertexData('vdata', format, Geom.UHStatic)
        self.vwriter = GeomVertexWriter(self.vdata, 'vertex')
        self.uvwriter = GeomVertexWriter(self.vdata, 'texcoord')
    
        # for offset vertices
        self.vdata2 = GeomVertexData('vdata2', format, Geom.UHStatic)
        self.vwriter2 = GeomVertexWriter(self.vdata2, 'vertex')
        # BIN fonts don't support offset, default value
        self.vwriter2.addData3f(1,0,0)
        
    # for each glyph:
    def readGlyph(self, index, file_object):
        # <short> character code (short = 2-byte integer)
        data_chunk = file_object.read(2) # Read character code, 2 bytes
        
        charcode = struct.unpack('<H', data_chunk)[0] # unsigned short/half (2 bytes)
        
        # <char*size*size> glyph data
        data_chunk = file_object.read(1* self.glyph_size* self.glyph_size)
        image = Image.frombuffer("L", (self.glyph_size, self.glyph_size), data_chunk, "raw", "L", 0, 1) # assign them all at once
        
        # Now the Panda3d objects
        # 6 vertices to make 2 triangles (quad)
        v0 = self.vwriter.addData3f(0,0,0)
        self.uvwriter.addData2f(self.xindex/float(self.gridsize), self.yindex/float(self.gridsize))
        v1 = self.vwriter.addData3f(0,0,1)
        self.uvwriter.addData2f(self.xindex/float(self.gridsize), (self.yindex+1)/float(self.gridsize))
        v2 = self.vwriter.addData3f(1,0,1)
        self.uvwriter.addData2f((self.xindex+1)/float(self.gridsize), (self.yindex+1)/float(self.gridsize))
        
        v3 = self.vwriter.addData3f(0,0,0)
        self.uvwriter.addData2f(self.xindex/float(self.gridsize), self.yindex/float(self.gridsize))
        v4 = self.vwriter.addData3f(1,0,0)
        self.uvwriter.addData2f((self.xindex+1)/float(self.gridsize), self.yindex/float(self.gridsize))
        v4 = self.vwriter.addData3f(1,0,1)
        self.uvwriter.addData2f((self.xindex+1)/float(self.gridsize), (self.yindex+1)/float(self.gridsize))
        
        # Create a GeomTriangles
        tris = GeomTriangles(Geom.UHStatic)
        tris.addVertices(index + 0, index + 1, index + 2)
        tris.closePrimitive()
        tris.addVertices(index + 3, index + 4, index + 5)
        tris.closePrimitive()
        
        quadgeom = Geom(self.vdata)
        quadgeom.addPrimitive(tris)
        
        # GeomPoint for the offset vertex
        point = GeomPoints(Geom.UHStatic)
        point.addVertex(0)
        pointgeom = Geom(self.vdata2)
        pointgeom.addPrimitive(point)
        
        try:
            gnode = GeomNode(self.charmap[str(charcode)])
        except KeyError:
            gnode = GeomNode(str(charcode))
        
        gnode.addGeom(quadgeom)
        gnode.addGeom(pointgeom)
        
        self.fontnode.addChild(gnode)
        
        self.fontimage.paste(image, (self.xindex * self.glyph_size, self.yindex * self.glyph_size))
    
    def loadBIN(self, filename):
        
        self.fontnode = PandaNode('bin_font')
    
        # BIN fonts are an in-house format for bitmap-based grayscale fonts
        
        # load the file to memory
        file_object = open(filename, 'rb') # read + binary
        
        # <char*4> signature "FONT"
        # the header will determine if this is a valid FONT file
        data_chunk = file_object.read(4) # Read header chunk
        if data_chunk=="FONT":
            print "This file is a valid BIN file!"
        else:
            print "This file is not a valid BIN file!"
            file_object.close()
            #sys.exit()
            return
        
        # <int> # of glyphs
        # read the next 4 bytes, which tells the number of "glyphs" in the file
        data_chunk = file_object.read(4) # of glyphs
        glyph_count = struct.unpack('<I', data_chunk)[0] # little endian, 4 bytes
        print "Number of glyphs is: "+ str(glyph_count)
        
        # <int> glyph size (in pixels)
        # read the next 4 bytes
        data_chunk = file_object.read(4) # glyph size?
        self.glyph_size = struct.unpack('<I', data_chunk)[0] # little endian, 4 bytes
        print "Glyph size (in pixels): "+ str(self.glyph_size)
        
        # ?, seems to be 0
        data_chunk = file_object.read(4)
        
        # make a PIL image based on the data we have
        self.gridsize = int(math.sqrt(glyph_count) + 1)
        pixelsize = self.gridsize * self.glyph_size
        
        self.fontimage = Image.new("L", (pixelsize, pixelsize))
        
        # for each glyph:
        self.xindex = 0
        self.yindex = 0
        for i in xrange(glyph_count):
            self.readGlyph(i, file_object)
            self.xindex += 1
            if self.xindex > self.gridsize:
                self.yindex += 1
                self.xindex = 0
                
        #self.fontimage.save(str("font") + '.png', 'PNG')
        width = self.fontimage.size[0]
        height = self.fontimage.size[1]
        
        data = self.fontimage.tostring()
        tex = Texture()
        tex.setup2dTexture(width, height, Texture.TUnsignedByte, Texture.FAlpha)
        tex.setRamImageAs(data, "A")
        
        fontNP = NodePath(self.fontnode)
        #texture = loader.loadTexture('font.png')
        fontNP.setTexture(tex)
        fontNP.setTransparency(TransparencyAttrib.MAlpha)
        #fontNP.ls()
        font = StaticTextFont(fontNP.node())
        #print font
        return font

fontloader = FontLoader()
font = fontloader.loadBIN('font.bin')

# test it
from direct.gui.OnscreenText import OnscreenText
textboxtext = OnscreenText(text = u'\x5c \xa2 \xa3 \xa7', fg = (1,1,1,1), scale = 0.3)
textboxtext.setAlign(TextNode.ALeft)
textboxtext.setFont(font)

run()

I don’t know what the UV’s are supposed to be, so I can’t tell you what’s wrong with these. But since you already got the UV’s working for the egg file version, why not just compare these with those?

David

no, I mean with PNMImage+Texture classes, like in the code above,not with PIL.

The tiny glyph images are merged to one, starting from top-left corner, the image is square.

I’m not following you.

Sorry, what?

Hmm, our communication seems to have broken down. :frowning:

Is there a specific question you were trying to ask me? If not, I’ll assume you’re on track to solving your problems.

David

Yes, my problem is I can’t get transparency working when using PNMImage, not PIL, like in my previous and your code.
Here are the relevant parts of the new code, these lines replace the PIL lines.

self.fontimage = PNMImage(pixelsize, pixelsize, 1)
...
datachunk = fileobject.read(1* self.glyphsize * self.glyphsize)
image = PNMImage(self.glyphsize, self.glyphsize, 1)
# because of the fact that PNMImage can't read() grayscale images, we need to create a Texture object instead and then convert it to PNMImage with store()
texture = Texture()
texture.setup2dTexture(self.glyphsize, self.glyphsize, Texture.TUnsignedByte, Texture.FAlpha)
texture.setRamImageAs(datachunk, "A")
texture.store(image)
...
self.fontimage.copySubImage(image, self.xindex * self.glyphsize, self.yindex * self.glyphsize)

Let me know if you prefer the full code. I think the problem is here though, this is what was changed.

You set up ‘texture’ as an alpha texture, but this isn’t the actual texture that you’re using for rendering, so that doesn’t mean much. It still gets written to the PNMImage as a simple one-channel image, which the PNMImage class interprets as a grayscale image.

Make sure you use Texture.FAlpha when you create the Texture you actually use for rendering. In fact, you might need to explicitly call tex.setMode(Texture.FAlpha) after you have read the PNMImage, because reading a grayscale image might implicitly set the texture to FLuminance mode even if you had previously set it to FAlpha mode (so you’ll need to set it back).

David

Texture doesn’t seem to have an attribute “setMode”

Sorry, I meant setFormat().

You’re right, that was the problem.
I still don’t seem to get UVs right. I create two triangles and one’s vertex UVs are correct, but not the other’s.

If “v” means vertex and “uv” is well uv, this is how you would assign a 32x32 texture to two triangles that make up a quad right?

# triangle 1
v1(0,0,0)
v2(0,0,1)
v3(1,0,1)

# triangle 2
v4(0,0,0)
v5(1,0,0)
v6(1,0,1)

# triangle 1 UVs
uv1(x,     1-y)
uv2(x,     1-(y+1))
uv3((x+1), 1-(y+1))

# triangle 2 UVs
uv4(x,     1-y)
uv5((x+1), 1-y)
uv6(x+1),  1-(y+1))

but second triangle has wrong UVs.

The actual code:

v0 = self.vwriter.addData3f(0,0,0)
		self.uvwriter.addData2f(self.xindex/float(self.gridsize), 1- self.yindex/float(self.gridsize))
		v1 = self.vwriter.addData3f(0,0,1)
		self.uvwriter.addData2f(self.xindex/float(self.gridsize), 1- (self.yindex+1)/float(self.gridsize))
		v2 = self.vwriter.addData3f(1,0,1)
		self.uvwriter.addData2f((self.xindex+1)/float(self.gridsize), 1- (self.yindex+1)/float(self.gridsize))
		
		v3 = self.vwriter.addData3f(0,0,0)
		self.uvwriter.addData2f(self.xindex/float(self.gridsize), 1- self.yindex/float(self.gridsize))
		v4 = self.vwriter.addData3f(1,0,0)
		self.uvwriter.addData2f((self.xindex+1)/float(self.gridsize), 1- (self.yindex)/float(self.gridsize))
		v4 = self.vwriter.addData3f(1,0,1)
		self.uvwriter.addData2f((self.xindex+1)/float(self.gridsize), 1- (self.yindex+1)/float(self.gridsize))

the full code:

import struct
import math # square root

from pandac.PandaModules import * # PNMImage
import direct.directbase.DirectStart

class FontLoader:
	def __init__(self):
		self.charmap = {}
		lines = open('jascii2unicode.txt').readlines()
		for i in lines:
			list = i.split('\t') # \t = tab
			self.charmap[str(int(list[0], 16))] = str(int(list[1], 16))
		
		format = GeomVertexFormat.getV3t2() # Panda3D pre-defines a handful of standard GeomVertexFormat. V3t2 supports vertices and UVs (all that we need)
	
		self.vdata = GeomVertexData('vdata', format, Geom.UHStatic)
		self.vwriter = GeomVertexWriter(self.vdata, 'vertex')
		self.uvwriter = GeomVertexWriter(self.vdata, 'texcoord')
	
		# for offset vertices
		self.vdata2 = GeomVertexData('vdata2', format, Geom.UHStatic)
		self.vwriter2 = GeomVertexWriter(self.vdata2, 'vertex')
		# BIN fonts don't support offset, default value
		self.vwriter2.addData3f(1,0,0)
		
	# for each glyph:
	def readGlyph(self, index, file_object):
		# <short> character code (short = 2-byte integer)
		data_chunk = file_object.read(2) # Read character code, 2 bytes
		
		charcode = struct.unpack('<H', data_chunk)[0] # unsigned short/half (2 bytes)
		
		# <char*size*size> glyph data
		data_chunk = file_object.read(1* self.glyph_size* self.glyph_size)
		
		image = PNMImage(self.glyph_size, self.glyph_size, 1)
		# because of the fact that PNMImage can't read() grayscale images, we need to create a Texture object instead and then convert it to PNMImage with store()
		texture = Texture()
		texture.setup2dTexture(self.glyph_size, self.glyph_size, Texture.TUnsignedByte, Texture.FAlpha)
		texture.setRamImageAs(data_chunk, "A")
		texture.store(image) 
		
		# Now the Panda3d objects
		# 6 vertices to make 2 triangles (quad)
		v0 = self.vwriter.addData3f(0,0,0)
		self.uvwriter.addData2f(self.xindex/float(self.gridsize), 1- self.yindex/float(self.gridsize))
		v1 = self.vwriter.addData3f(0,0,1)
		self.uvwriter.addData2f((self.xindex)/float(self.gridsize), 1- (self.yindex+1)/float(self.gridsize))
		v2 = self.vwriter.addData3f(1,0,1)
		self.uvwriter.addData2f((self.xindex+1)/float(self.gridsize), 1- (self.yindex+1)/float(self.gridsize))
		
		v3 = self.vwriter.addData3f(0,0,0)
		self.uvwriter.addData2f(self.xindex/float(self.gridsize), 1- self.yindex/float(self.gridsize))
		v4 = self.vwriter.addData3f(1,0,0)
		self.uvwriter.addData2f((self.xindex+1)/float(self.gridsize), 1- (self.yindex)/float(self.gridsize))
		v4 = self.vwriter.addData3f(1,0,1)
		self.uvwriter.addData2f((self.xindex+1)/float(self.gridsize), 1- (self.yindex+1)/float(self.gridsize))
		
		# Create a GeomTriangles
		tris = GeomTriangles(Geom.UHStatic)
		tris.addVertices(index + 0, index + 1, index + 2)
		tris.closePrimitive()
		tris.addVertices(index + 3, index + 4, index + 5)
		tris.closePrimitive()
		
		quadgeom = Geom(self.vdata)
		quadgeom.addPrimitive(tris)
		
		# GeomPoint for the offset vertex
		point = GeomPoints(Geom.UHStatic)
		point.addVertex(0)
		pointgeom = Geom(self.vdata2)
		pointgeom.addPrimitive(point)
		
		try:
			gnode = GeomNode(self.charmap[str(charcode)])
		except KeyError:
			gnode = GeomNode(str(charcode))
		
		gnode.addGeom(quadgeom)
		gnode.addGeom(pointgeom)
		
		self.fontnode.addChild(gnode)
		
		self.fontimage.copySubImage(image, self.xindex * self.glyph_size, self.yindex * self.glyph_size)
	
	def loadBIN(self, filename):
		
		self.fontnode = PandaNode('bin_font')
	
		# BIN fonts are an in-house format for bitmap-based grayscale fonts
		
		# load the file to memory
		file_object = open(filename, 'rb') # read + binary
		
		# <char*4> signature "FONT"
		# the header will determine if this is a valid FONT file
		data_chunk = file_object.read(4) # Read header chunk
		if data_chunk=="FONT":
			print "This file is a valid BIN file!"
		else:
			print "This file is not a valid BIN file!"
			file_object.close()
			#sys.exit()
			return
		
		# <int> # of glyphs
		# read the next 4 bytes, which tells the number of "glyphs" in the file
		data_chunk = file_object.read(4) # of glyphs
		glyph_count = struct.unpack('<I', data_chunk)[0] # little endian, 4 bytes
		print "Number of glyphs is: "+ str(glyph_count)
		
		# <int> glyph size (in pixels)
		# read the next 4 bytes
		data_chunk = file_object.read(4) # glyph size?
		self.glyph_size = struct.unpack('<I', data_chunk)[0] # little endian, 4 bytes
		print "Glyph size (in pixels): "+ str(self.glyph_size)
		
		# ?, seems to be 0
		data_chunk = file_object.read(4)
		
		# make a PIL image based on the data we have
		self.gridsize = int(math.sqrt(glyph_count) + 1)
		pixelsize = self.gridsize * self.glyph_size
		
		self.fontimage = PNMImage(pixelsize, pixelsize, 1) 
		
		# for each glyph:
		self.xindex = 0
		self.yindex = 0
		for i in xrange(glyph_count):
			self.readGlyph(i, file_object)
			self.xindex += 1
			if self.xindex > self.gridsize:
				self.yindex += 1
				self.xindex = 0
		
		tex = Texture()
		tex.load(self.fontimage)
		tex.setFormat(Texture.FAlpha)
		
		fontNP = NodePath(self.fontnode)
		fontNP.setTexture(tex)
		fontNP.setTransparency(TransparencyAttrib.MAlpha)
		font = StaticTextFont(fontNP.node())
		return font

fontloader = FontLoader()
font = fontloader.loadBIN('font.bin')

# test it
from direct.gui.OnscreenText import OnscreenText
textboxtext = OnscreenText(text = u'\x5c\xa2\xa3\xa7' , fg = (1,1,1,1), scale = 0.3)
textboxtext.setAlign(TextNode.ALeft)
textboxtext.setFont(font)

run()

I checked some stuff. Disable textures and you’ll see that the second triangle was created. Change the UVs to some randomness and you’ll see that it’s not a “normals” problem ( I also tried fontNP.setTwoSided(True) to be sure).

You are passing in the wrong index number to readGlyph(). You are passing in the glyph index number, but then you are using that number as if it were the vertex index number, which means you’re off by a factor of 6 (since you create six vertices per glyph).

I corrected this problem by inserting the line “index = index * 6” to the readGlyph() method. Alternatively, you can replace this with "index = self.vwriter.getWriteRow() before your first call to self.vwriter.addData3f() in readGlyph().

David

Thank you, that fixed everything.

I have a general Python question but I’m not really able to explain it anywhere else properly.
Basically the character codes (which are shift-jis codes) are stored in the font file as little-endian 2 byte unsigned shorts. Right now I parse some file containing shift-jis codes and their corresponding unicode codes, generate a dictionary and get the needed numbers to pass to the GeomNodes like this, by passing the number I read from the font file as a dictionary key each time.

However, I don’t think this is necessary, takes more time and complicates the code.
My question is, can I just read those shorts and convert them to unicode character codes using Python’s standard library?

My guess is I could somehow read the number, convert it to a string of shift-jis encoding, convert that string to a unicode string, then convert the unicode string to an integer (character code) to finally pass that value as the GeomNode’s name.

You just want to convert a two-byte binary string to a Python integer? Have you tried the Python struct module?

David

Yes, I use the module struct, but like I said it will return an integer which is a shift-jis character code, not utf8 which I need.

OK, but I don’t understand what part of that process needs improvement. A dictionary lookup is as close to free as Python can get you.

I guess you could bake the whole pipeline into one dictionary: build the dictionary of binary strings to GeomNode names.

David

Actually parsing the file I have now and creating the dictionary takes time. And I don’t think I need a 200 kilobyte file inside my sourcecode, and I don’t think that it’s very elegant.
So if I could use Python’s standard library to convert the shift-jis integers to unicode integers (for example the struct module+string methods for converting between encoding types) it would be faster and cleaner (also less confusing for people who would read the code).
Don’t know how though.

Well, at some point somewhere, you need to have a table mapping shift-jis to Unicode. Whether that table is stored in a dictionary or in a text file or in a SQL database or wherever, the data in that table has to be a part of your program. So you’re trying to decide the most efficient way to store the table; and there are of course lots of possible solutions. You have to decide how to balance load-time optimization with file-size optimization.

I suppose there might be a standard Python module that already includes that table in some form, but I’m not aware of it if there is.

David