-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathmakefont.html
192 lines (191 loc) · 11.2 KB
/
makefont.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
<script src="opentype.min.js"></script>
<script src="UPNG.js"></script>
<script src="jszip.min.js"></script>
<script>
function extractJar(blob,amplifier,centerA) { //unzip minecraft.jar
console.log("Amplifier: "+amplifier)
document.getElementById("st").innerText="Making font (see console for more detail)"
var commonMultiple=1
var glyphs={0:new opentype.Glyph({
name:".notdef",
unicode:0,
advanceWidth:4*amplifier,
path:new opentype.Path()
})} //initializes a glyphs object with only the null glyph
console.log("Unzipping jar")
const jarReader=new FileReader();
jarReader.readAsArrayBuffer(blob);
jarReader.onload=async function(e) {
const zip=await JSZip.loadAsync(jarReader.result) //zip is set to the extracted zip object
const providers=await extractCharMap(zip)
var totalGlyphCount=0;
for (var i=0;i<providers.length;i++) { //i cycles through each provider
var imagepath=undefined;
if (providers[i].type=="bitmap") { //set imagepath variable based on provider type
imagepath="assets/"+providers[i].file.split(":").join("/textures/")
} else if (providers[i].type=="bitmap_legacy_tp") {
imagepath="font/default.png"
}
if (imagepath) {
console.log("Reading provider "+providers[i].file)
var image=UPNG.decode(await zip.file(imagepath).async("arraybuffer"))
var imagemap=new DataView(UPNG.toRGBA8(image)[0]) //get image using file path
var ascent=providers[i].ascent
var charsPerRow=providers[i].chars[0].replace(/[\udc00-\udfff]/g,"").length
var charsPerCol=providers[i].chars.length
var height=image.height/charsPerCol
var width=image.width/charsPerRow
//new scaling support code
var heightScale=height/(providers[i].height || 8)
var GCF=[Math.max(commonMultiple,heightScale),Math.min(commonMultiple,heightScale)]
while (GCF[1]>0) {GCF.push(GCF[0]%GCF[1]);GCF.shift()}
GCF=(commonMultiple*heightScale)/GCF[0]//GCF is now new common multiple
if (commonMultiple!=GCF) {
for (var j in glyphs) {glyphs[j].advanceWidth*=(GCF/commonMultiple);for (var k=0;k<glyphs[j].path.commands.length;k++) {glyphs[j].path.commands[k].x*=(GCF/commonMultiple);glyphs[j].path.commands[k].y*=(GCF/commonMultiple)}} //scale each glyph to new size
console.log("Scaling glyphs by "+(GCF/commonMultiple))
commonMultiple=GCF
}
var providerscale=(commonMultiple/heightScale)
for (var j=0;j<charsPerRow*charsPerCol;j++) { //j cycles through each character
var currentRow=providers[i].chars[Math.floor(j/charsPerRow)]
for (var k=0,l=0;k<j%charsPerRow;k++) {if (currentRow.codePointAt(k+l)>0xffff) l++} //finds jth code point, not jth word
var currentcodepoint=currentRow.codePointAt(k+l)||0;//sets current code point
if (!(glyphs[currentcodepoint])) { //only add char if not already defined
totalGlyphCount++
var glyphPath=new opentype.Path();
var l=[] //l will keep track of the rightmost pixel of the char
var topLeftPixel=((j%charsPerRow)*width)+(Math.floor(j/charsPerRow)*image.width*height)
for (var k=0;k<height*width;k++) { //k cycles through the pixels in the char
var currentpixel=(Math.floor(k/width)*image.width)+(k%width)+topLeftPixel //set currentpixel to pixel in image
l[k%width]=(l[k%width]||0)+imagemap.getUint8(currentpixel*4+3) //add alpha value to current column in l
if (imagemap.getUint8(currentpixel*4+3)>127) { //if pixel is visible enough to be in font
//convert k to ttf coordinate system
var ttfy=((ascent*(commonMultiple/providerscale))-(Math.floor(k/width)))*amplifier*providerscale,ttfx=((k%width)*amplifier*providerscale)+(!!centerA*(amplifier/2)*providerscale)
var leftLine=glyphPath.commands.findIndex((ele,ind,arr)=>ind+1<arr.length&&ele.x==ttfx&&ele.y>=ttfy&&arr[ind+1].x==ttfx&&arr[ind+1].y<ttfy) //check if a line is on the left
if (leftLine>-1) {
//attach left line
glyphPath.commands.splice(leftLine+(glyphPath.commands[leftLine].y!=ttfy),1+(glyphPath.commands[leftLine].y==ttfy),...([{type:"L",x:ttfx,y:ttfy},{type:"L",x:ttfx+(amplifier*providerscale),y:ttfy},{type:"L",x:ttfx+(amplifier*providerscale),y:ttfy-(amplifier*providerscale)}].slice(glyphPath.commands[leftLine].y==ttfy)))
//check if line on top
var topLine=glyphPath.commands.findIndex((ele,ind,arr)=>ind+1<arr.length&&ele.y==ttfy&&ele.x>ttfx&&arr[ind+1].y==ttfy&&arr[ind+1].x<=ttfx)
if (topLine>-1) { //this is the tricky bit
for (var leftClumpStart=leftLine;leftClumpStart>-1;leftClumpStart--) if (glyphPath.commands[leftClumpStart].type=="M") break;
for (var leftClumpEnd=leftLine;leftClumpEnd<glyphPath.commands.length;leftClumpEnd++) if (glyphPath.commands[leftClumpEnd].type=="Z") break;
if (topLine>leftClumpStart&&topLine<leftClumpEnd) { //forms a loop
//if (topLine<leftLine) { //path start is outside the loop
var absorb=glyphPath.commands.splice(topLine+1,leftLine-topLine-1);
absorb[0].type="M"
absorb.push({type:"Z"})
glyphPath.commands.unshift(...absorb)
topLine+=absorb.length
} else { //no loop
if (topLine<leftClumpEnd) { //top clump is earlier
var absorb=[glyphPath.commands.splice(leftLine,1+leftClumpEnd-leftLine),glyphPath.commands.splice(leftClumpStart,leftLine-leftClumpStart)]
absorb[0].pop()
absorb[1][0].type="L"
glyphPath.commands.splice(topLine+1,0,...(absorb[0].concat(absorb[1])))
} else { //left clump is earlier; merge top clump into left
for (var topClumpStart=topLine;topClumpStart>-1;topClumpStart--) if (glyphPath.commands[topClumpStart].type=="M") break;
for (var topClumpEnd=topLine;topClumpEnd<glyphPath.commands.length;topClumpEnd++) if (glyphPath.commands[topClumpEnd].type=="Z") break;
var absorb=[glyphPath.commands.splice(topLine+1,topClumpEnd-topLine),glyphPath.commands.splice(topClumpStart,1+topLine-topClumpStart)]
absorb[0].pop()
absorb[1][0].type="L"
glyphPath.commands.splice(leftLine,0,...(absorb[0].concat(absorb[1])))
topLine=leftLine+absorb[0].length+absorb[1].length-1
}
}
if (glyphPath.commands[topLine].x==glyphPath.commands[topLine+1].x) glyphPath.commands.splice(topLine,2)
} else if (glyphPath.commands[leftLine].x==glyphPath.commands[leftLine-1].x) glyphPath.commands.splice(leftLine-1,2)
} else {
//check if line on top
var topLine=glyphPath.commands.findIndex((ele,ind,arr)=>ind+1<arr.length&&ele.y==ttfy&&ele.x>ttfx&&arr[ind+1].y==ttfy&&arr[ind+1].x<=ttfx)
if (topLine>-1) {
glyphPath.commands.splice(topLine+(glyphPath.commands[topLine].x!=ttfx+(amplifier*providerscale)),(glyphPath.commands[topLine].x==ttfx+(amplifier*providerscale))+(glyphPath.commands[topLine+1].x==ttfx),...([{type:"L",x:ttfx+(amplifier*providerscale),y:ttfy},{type:"L",x:ttfx+(amplifier*providerscale),y:ttfy-(amplifier*providerscale)},{type:"L",x:ttfx,y:ttfy-(amplifier*providerscale)},{type:"L",x:ttfx,y:ttfy}].slice(glyphPath.commands[topLine].x==ttfx+(amplifier*providerscale),3+(glyphPath.commands[topLine+1].x!=ttfx))))
} else {
//create the new square
glyphPath.commands.push({type:"M",x:ttfx,y:ttfy},{type:"L",x:ttfx+(amplifier*providerscale),y:ttfy},{type:"L",x:ttfx+(amplifier*providerscale),y:ttfy-(amplifier*providerscale)},{type:"L",x:ttfx,y:ttfy-(amplifier*providerscale)},{type:"Z"})
}
}
}
}
//find rightmost pixel and create glyph (make exception for blank glyphs)
var k=l.length-1;
while (l[k]==0&&k>-1) k-- //k is set to rightmost column with pixels
var l=currentcodepoint.toString(16);while (l.length<4) l="0"+l; //prepare unicode code for current code point
glyphs[currentcodepoint]=new opentype.Glyph({
name: "uni"+l,
unicode:currentcodepoint,
advanceWidth: (k==-1?(4*commonMultiple):((k+2)*providerscale))*amplifier, //width is set to 4 if character is empty
path:glyphPath
})
//console.log("Added glyph "+String.fromCodePoint(currentcodepoint))
} else {console.log("Skipped duplicate glyph "+String.fromCodePoint(currentcodepoint))}
}
} else {console.log("Skipping provider "+providers[i].type)}
}
//turn glyphs object into an array
var finalGlyphArray=[]
for (j in glyphs) {finalGlyphArray.push(glyphs[j])}
//create final font
var finalFont= new opentype.Font({
familyName:"Mojangles"+(!centerA?" left-aligned":""),
styleName:blob.name.split(".").slice(0,-1).join("."),
unitsPerEm:9*commonMultiple*amplifier,
ascender:8*commonMultiple*amplifier,
descender:-1*commonMultiple*amplifier,
glyphs: finalGlyphArray //temp test of only null and "
});
console.log(totalGlyphCount+" glyphs")
finalFont.download()
document.getElementById("st").innerText=""
}
}
async function extractCharMap(zip) { //get char map from default.txt, or from predefined maps if it doesn't exist
console.log("Finding character map")
var cp437=[
"\u0000\u263a\u263b\u2665\u2666\u2663\u2660\u2022\u25d8\u25cb\u25d9\u2642\u2640\u266a\u266b\u263c",
"\u25ba\u25c4\u2195\u203c\u00b6\u00a7\u25ac\u21a8\u2191\u2193\u2192\u2190\u221f\u2194\u25b2\u25bc",
" !\"#$%&'()*+,-./",
"0123456789:;<=>?",
"@ABCDEFGHIJKLMNO",
"PQRSTUVWXYZ[\\]^_",
"`abcdefghijklmno",
"pqrstuvwxyz{\u00a6}~\u2302",
"\u00c7\u00fc\u00e9\u00e2\u00e4\u00e0\u00e5\u00e7\u00ea\u00eb\u00e8\u00ef\u00ee\u00ec\u00c4\u00c5",
"\u00c9\u00e6\u00c6\u00f4\u00f6\u00f2\u00fb\u00f9\u00ff\u00d6\u00dc\u00a2\u00a3\u00a5\u20a7\u0192",
"\u00e1\u00ed\u00f3\u00fa\u00f1\u00d1\u00aa\u00ba\u00bf\u2310\u00ac\u00bd\u00bc\u00a1\u00ab\u00bb",
"\u2591\u2592\u2593\u2502\u2524\u2561\u2562\u2556\u2555\u2563\u2551\u2557\u255d\u255c\u255b\u2510",
"\u2514\u2534\u252c\u251c\u2500\u253c\u255e\u255f\u255a\u2554\u2569\u2566\u2560\u2550\u256c\u2567",
"\u2568\u2564\u2565\u2559\u2558\u2552\u2553\u256b\u256a\u2518\u250c\u2588\u2584\u258c\u2590\u2580",
"\u03b1\u03b2\u0393\u03c0\u03a3\u03c3\u00b5\u03c4\u03a6\u0398\u03a9\u03b4\u221e\u2205\u2208\u2229",
"\u2261\u00b1\u2265\u2264\u2320\u2321\u00f7\u2248\u00b0\u2219\u00b7\u221a\u207f\u00b2\u25a0\u00a0"
],cp437m=[
"\u00c0\u00c1\u00c2\u00c8\u00ca\u00cb\u00cd\u00d3\u00d4\u00d5\u00da\u00df\u0101\u014d\u011f\u0130",
"\u0131\u0152\u0153\u015e\u015f\u0174\u0175\u017e\u00ea"
].concat(cp437.slice(2));
if (zip.file("pack.mcmeta")) { //get pack version from pack.mcmeta if it exists
var version=JSON.parse(await zip.file("pack.mcmeta").async("string")).pack.pack_format
if (version>=4) { //default.json mapping exists
if (version>6) {console.log("Future version detected. Continuing with unknown support.")}
return JSON.parse(await zip.file("assets/minecraft/font/default.json").async("string")).providers
} else if (version==1) { //pack.mcmeta exists, code page 437
return [{type:"bitmap",file:"minecraft:font/ascii.png",ascent:7,chars:cp437}]
} else { //pack.mcmeta exists, modified cp437
return [{type:"bitmap",file:"minecraft:font/ascii.png",ascent:7,chars:cp437m}]
}
} else if (zip.file("assets/minecraft/textures/font/ascii.png")) { //pack.mcmeta does not exist, modified cp437
return [{type:"bitmap",file:"minecraft:font/ascii.png",ascent:7,chars:cp437m}]
} else if (zip.file("font/default.png")) { //pack.mcmeta does not exist, cp437
return [{type:"bitmap_legacy_tp",ascent:7,chars:cp437,file:"legacy texture pack"}]
} else { //no font detected
console.warn("Error: No font detected")
}
}
</script>
<h1>McJar Font Getter</h1>
Extracts the "Mojangles" font from a Minecraft .jar file<br><br>
<input type="checkbox" checked id="centerA"><label for="centerA">Center-align glyphs (uncheck for left-align)</label><br>
<label for="minecraftJar">Choose a Minecraft .jar file</label>
<input id="minecraftJar" type="file" onChange='extractJar(this.files[0],2,document.getElementById("centerA").checked)'><span id="st"></span><br><br>
Components are used under <a href="LICENSE">MIT License</a>.<br>
See the <a href="README.md">readme</a> for instructions on getting a Minecraft .jar file.<br>
Version 1.5. Check the <a href="CHANGELOG">changelog</a> for changes.