1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one or more 3 * contributor license agreements. See the NOTICE file distributed with 4 * this work for additional information regarding copyright ownership. 5 * The ASF licenses this file to You under the Apache License, Version 2.0 6 * (the "License"); you may not use this file except in compliance with 7 * the License. You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18 /* $Id$ */ 19 20 package org.apache.fop.fonts.truetype; 21 22 import java.io.ByteArrayInputStream; 23 import java.io.DataInputStream; 24 import java.io.File; 25 import java.io.IOException; 26 import java.util.ArrayList; 27 import java.util.Arrays; 28 import java.util.Collections; 29 import java.util.HashMap; 30 import java.util.LinkedHashMap; 31 import java.util.List; 32 import java.util.Map; 33 34 import org.junit.Assert; 35 import org.junit.Before; 36 import org.junit.Test; 37 38 import static org.junit.Assert.assertEquals; 39 import static org.junit.Assert.assertTrue; 40 import static org.mockito.Matchers.any; 41 import static org.mockito.Mockito.mock; 42 import static org.mockito.Mockito.when; 43 44 import org.apache.fontbox.cff.CFFFont; 45 import org.apache.fontbox.cff.CFFParser; 46 import org.apache.fontbox.cff.CFFType1Font; 47 import org.apache.fontbox.cff.CharStringCommand; 48 import org.apache.fontbox.cff.Type2CharString; 49 50 import org.apache.fop.fonts.MultiByteFont; 51 import org.apache.fop.fonts.cff.CFFDataReader; 52 import org.apache.fop.fonts.cff.CFFDataReader.CFFIndexData; 53 import org.apache.fop.fonts.cff.CFFDataReader.DICTEntry; 54 import org.apache.fop.fonts.truetype.OTFSubSetFile.BytesNumber; 55 56 public class OTFSubSetFileTestCase extends OTFFileTestCase { 57 private Map<Integer, Integer> glyphs = new HashMap<Integer, Integer>(); 58 59 /** 60 * Initialises the test by creating the font subset. A CFFDataReader is 61 * also created based on the subset data for use in the tests. 62 * @throws IOException 63 */ 64 @Before setUp()65 public void setUp() throws Exception { 66 super.setUp(); 67 for (int i = 0; i < 256; i++) { 68 glyphs.put(i, i); 69 } 70 } 71 getCFFReaderSourceSans()72 private CFFDataReader getCFFReaderSourceSans() throws IOException { 73 byte[] sourceSansData = getSourceSansSubset().getFontSubset(); 74 return new CFFDataReader(sourceSansData); 75 } 76 getSourceSansSubset()77 private OTFSubSetFile getSourceSansSubset() throws IOException { 78 OTFSubSetFile sourceSansSubset = new OTFSubSetFile(); 79 sourceSansSubset.readFont(sourceSansReader, "SourceSansProBold", null, glyphs); 80 return sourceSansSubset; 81 } 82 83 /** 84 * Validates the CharString data against the original font 85 * @throws IOException 86 */ 87 @Test testCharStringIndex()88 public void testCharStringIndex() throws IOException { 89 CFFDataReader cffReaderSourceSans = getCFFReaderSourceSans(); 90 assertEquals(256, cffReaderSourceSans.getCharStringIndex().getNumObjects()); 91 assertTrue(checkCorrectOffsets(cffReaderSourceSans.getCharStringIndex())); 92 validateCharStrings(cffReaderSourceSans, getSourceSansSubset().getCFFReader()); 93 } 94 95 /** 96 * Checks the index data to ensure that the offsets are valid 97 * @param indexData The index data to check 98 * @return Returns true if it is found to be valid 99 */ checkCorrectOffsets(CFFIndexData indexData)100 private boolean checkCorrectOffsets(CFFIndexData indexData) { 101 int last = 0; 102 for (int i = 0; i < indexData.getOffsets().length; i++) { 103 if (indexData.getOffsets()[i] < last) { 104 return false; 105 } 106 last = indexData.getOffsets()[i]; 107 } 108 return true; 109 } 110 111 /** 112 * Validates the subset font CharString data by comparing it with the original. 113 * @param subsetCFF The subset CFFDataReader containing the CharString data 114 * @param origCFF The original CFFDataReader containing the CharString data 115 * @throws IOException 116 */ validateCharStrings(CFFDataReader subsetCFF, CFFDataReader origCFF)117 private void validateCharStrings(CFFDataReader subsetCFF, CFFDataReader origCFF) 118 throws IOException { 119 CFFFont sourceSansOriginal = sourceSansProBold.fileFont; 120 CFFIndexData charStrings = subsetCFF.getCharStringIndex(); 121 List<byte[]> origCharStringData = sourceSansOriginal.getCharStringBytes(); 122 for (int i = 0; i < charStrings.getNumObjects(); i++) { 123 byte[] origCharData = origCharStringData.get(i); 124 byte[] charData = charStrings.getValue(i); 125 List<BytesNumber> origOperands = getFullCharString(new Context(), origCharData, origCFF); 126 List<BytesNumber> subsetOperands = getFullCharString(new Context(), charData, subsetCFF); 127 for (int j = 0; j < origOperands.size(); j++) { 128 assertTrue(origOperands.get(j).equals(subsetOperands.get(j))); 129 } 130 } 131 } 132 133 static class Context { 134 private ArrayList<BytesNumber> operands = new ArrayList<BytesNumber>(); 135 private ArrayList<BytesNumber> stack = new ArrayList<BytesNumber>(); 136 private int hstemCount; 137 private int vstemCount; 138 private int lastOp = -1; 139 private int maskLength = -1; 140 pushOperand(BytesNumber v)141 public void pushOperand(BytesNumber v) { 142 operands.add(v); 143 if (v instanceof Operator) { 144 if (v.getNumber() != 11 && v.getNumber() != 12) { 145 lastOp = v.getNumber(); 146 } 147 } else { 148 stack.add(v); 149 } 150 } 151 popOperand()152 public BytesNumber popOperand() { 153 operands.remove(operands.size() - 1); 154 return stack.remove(stack.size() - 1); 155 } 156 lastOperand()157 public BytesNumber lastOperand() { 158 return operands.get(operands.size() - 1); 159 } 160 clearStack()161 public void clearStack() { 162 stack.clear(); 163 } 164 getMaskLength()165 public int getMaskLength() { 166 // The number of data bytes for mask is exactly the number needed, one 167 // bit per hint, to reference the number of stem hints declared 168 // at the beginning of the charstring program. 169 if (maskLength > 0) { 170 return maskLength; 171 } 172 return 1 + (hstemCount + vstemCount - 1) / 8; 173 } 174 getFullOperandsList()175 public List<BytesNumber> getFullOperandsList() { 176 return operands; 177 } 178 countHstem()179 public void countHstem() { 180 // hstem(hm) operator 181 hstemCount += stack.size() / 2; 182 clearStack(); 183 } 184 countVstem()185 public void countVstem() { 186 // vstem(hm) operator 187 vstemCount += stack.size() / 2; 188 clearStack(); 189 } 190 calcMaskLength()191 public int calcMaskLength() { 192 if (lastOp == 1 || lastOp == 18) { 193 //If hstem and vstem hints are both declared at the beginning of 194 //a charstring, and this sequence is followed directly by the 195 //hintmask or cntrmask operators, the vstem hint operator need 196 //not be included. 197 vstemCount += stack.size() / 2; 198 } 199 clearStack(); 200 return getMaskLength(); 201 } 202 } 203 /** 204 * Recursively reads and constructs the full CharString for comparison 205 * @param data The original byte data of the CharString 206 * @param cffData The CFFDataReader containing the subroutine indexes 207 * @return Returns a list of parsed operands and operators 208 * @throws IOException 209 */ getFullCharString(Context context, byte[] data, CFFDataReader cffData)210 private List<BytesNumber> getFullCharString(Context context, byte[] data, CFFDataReader cffData) 211 throws IOException { 212 CFFIndexData localIndexSubr = cffData.getLocalIndexSubr(); 213 CFFIndexData globalIndexSubr = cffData.getGlobalIndexSubr(); 214 boolean hasLocalSubroutines = localIndexSubr != null && localIndexSubr.getNumObjects() > 0; 215 boolean hasGlobalSubroutines = globalIndexSubr != null && globalIndexSubr.getNumObjects() > 0; 216 for (int dataPos = 0; dataPos < data.length; dataPos++) { 217 int b0 = data[dataPos] & 0xff; 218 if (b0 == 10 && hasLocalSubroutines) { 219 int subrNumber = getSubrNumber(localIndexSubr.getNumObjects(), 220 context.popOperand().getNumber()); 221 byte[] subr = localIndexSubr.getValue(subrNumber); 222 getFullCharString(context, subr, cffData); 223 } else if (b0 == 29 && hasGlobalSubroutines) { 224 int subrNumber = getSubrNumber(globalIndexSubr.getNumObjects(), 225 context.popOperand().getNumber()); 226 byte[] subr = globalIndexSubr.getValue(subrNumber); 227 getFullCharString(context, subr, cffData); 228 } else if ((b0 >= 0 && b0 <= 27) || (b0 >= 29 && b0 <= 31)) { 229 int size = 1; 230 int b1 = -1; 231 if (b0 == 12) { 232 b1 = data[dataPos++] & 0xff; 233 size = 2; 234 } else if (b0 == 1 || b0 == 18) { 235 context.countHstem(); 236 } else if (b0 == 3 || b0 == 23) { 237 context.countVstem(); 238 } else if (b0 == 19 || b0 == 20) { 239 int length = context.calcMaskLength(); 240 dataPos += length; 241 size = length + 1; 242 } 243 context.pushOperand(new Operator(b0, size, getOperatorName(b0, b1))); 244 } else if (b0 == 28 || (b0 >= 32 && b0 <= 255)) { 245 context.pushOperand(readNumber(b0, data, dataPos)); 246 dataPos += context.lastOperand().getNumBytes() - 1; 247 } 248 } 249 return context.getFullOperandsList(); 250 } 251 252 /** 253 * Parses a number from one or more bytes 254 * @param b0 The first byte to identify how to interpret the number 255 * @param input The original byte data containing the number 256 * @param curPos The current position of the number 257 * @return Returns the number 258 * @throws IOException 259 */ readNumber(int b0, byte[] input, int curPos)260 private BytesNumber readNumber(int b0, byte[] input, int curPos) throws IOException { 261 if (b0 == 28) { 262 int b1 = input[curPos + 1] & 0xff; 263 int b2 = input[curPos + 2] & 0xff; 264 return new BytesNumber((int) (short) (b1 << 8 | b2), 3); 265 } else if (b0 >= 32 && b0 <= 246) { 266 return new BytesNumber(b0 - 139, 1); 267 } else if (b0 >= 247 && b0 <= 250) { 268 int b1 = input[curPos + 1] & 0xff; 269 return new BytesNumber((b0 - 247) * 256 + b1 + 108, 2); 270 } else if (b0 >= 251 && b0 <= 254) { 271 int b1 = input[curPos + 1] & 0xff; 272 return new BytesNumber(-(b0 - 251) * 256 - b1 - 108, 2); 273 } else if (b0 == 255) { 274 int b1 = input[curPos + 1] & 0xff; 275 int b2 = input[curPos + 2] & 0xff; 276 int b3 = input[curPos + 3] & 0xff; 277 int b4 = input[curPos + 4] & 0xff; 278 return new BytesNumber((b1 << 24 | b2 << 16 | b3 << 8 | b4), 5); 279 } else { 280 throw new IllegalArgumentException(); 281 } 282 } 283 284 /** 285 * Gets the subroutine number according to the number of subroutines 286 * and the provided operand. 287 * @param numSubroutines The number of subroutines used to calculate the 288 * subroutine reference. 289 * @param operand The operand for the subroutine 290 * @return Returns the calculated subroutine number 291 */ getSubrNumber(int numSubroutines, int operand)292 private int getSubrNumber(int numSubroutines, int operand) { 293 int bias = getBias(numSubroutines); 294 return bias + operand; 295 } 296 297 /** 298 * Gets the bias give the number of subroutines. This is used in the 299 * calculation to determine a subroutine's number 300 * @param subrCount The number of subroutines for a given index 301 * @return Returns the bias value 302 */ getBias(int subrCount)303 private int getBias(int subrCount) { 304 if (subrCount < 1240) { 305 return 107; 306 } else if (subrCount < 33900) { 307 return 1131; 308 } else { 309 return 32768; 310 } 311 } 312 313 /** 314 * A class representing an operator from the CharString data 315 */ 316 private class Operator extends BytesNumber { 317 private String opName = ""; 318 Operator(int number, int numBytes, String opName)319 Operator(int number, int numBytes, String opName) { 320 super(number, numBytes); 321 this.opName = opName; 322 } 323 toString()324 public String toString() { 325 return String.format("[%s]", opName); 326 } 327 } 328 329 /** 330 * Gets the identifying name for the given operator. This is primarily 331 * used for debugging purposes. See the Type 2 CharString Format specification 332 * document (Technical Note #5177) Appendix A (Command Codes). 333 * @param operator The operator code 334 * @param operatorB The second byte of the operator 335 * @return Returns the operator name. 336 */ getOperatorName(int operator, int operatorB)337 private String getOperatorName(int operator, int operatorB) { 338 switch (operator) { 339 case 0: return "Reserved"; 340 case 1: return "hstem"; 341 case 2: return "Reserved"; 342 case 3: return "vstem"; 343 case 4: return "vmoveto"; 344 case 5: return "rlineto"; 345 case 6: return "hlineto"; 346 case 7: return "vlineto"; 347 case 8: return "rrcurveto"; 348 case 9: return "Reserved"; 349 case 10: return "callsubr"; 350 case 11: return "return"; 351 case 12: return getDoubleOpName(operatorB); 352 case 13: return "Reserved"; 353 case 14: return "enchar"; 354 case 15: 355 case 16: 356 case 17: return "Reserved"; 357 case 18: return "hstemhm"; 358 case 19: return "hintmask"; 359 case 20: return "cntrmask"; 360 case 21: return "rmoveto"; 361 case 22: return "hmoveto"; 362 case 23: return "vstemhm"; 363 case 24: return "rcurveline"; 364 case 25: return "rlinecurve"; 365 case 26: return "vvcurveto"; 366 case 27: return "hhcurveto"; 367 case 28: return "shortint"; 368 case 29: return "callgsubr"; 369 case 30: return "vhcurveto"; 370 case 31: return "hvcurveto"; 371 default: return "Unknown"; 372 } 373 } 374 375 /** 376 * Gets the name of a double byte operator code 377 * @param operator The second byte of the operator 378 * @return Returns the name 379 */ getDoubleOpName(int operator)380 private String getDoubleOpName(int operator) { 381 switch (operator) { 382 case 0: 383 case 1: 384 case 2: return "Reserved"; 385 case 3: return "and"; 386 case 4: return "or"; 387 case 5: return "not"; 388 case 6: 389 case 7: 390 case 8: return "Reserved"; 391 case 9: return "abs"; 392 case 10: return "add"; 393 case 11: return "sub"; 394 case 12: return "div"; 395 case 13: return "Reserved"; 396 case 14: return "neg"; 397 case 15: return "eq"; 398 case 16: 399 case 17: return "Reserved"; 400 case 18: return "drop"; 401 case 19: return "Reserved"; 402 case 20: return "put"; 403 case 21: return "get"; 404 case 22: return "ifelse"; 405 case 23: return "random"; 406 case 24: return "mul"; 407 case 25: return "Reserved"; 408 case 26: return "sqrt"; 409 case 27: return "dup"; 410 case 28: return "exch"; 411 case 29: return "index"; 412 case 30: return "roll"; 413 case 31: 414 case 32: 415 case 33: return "Reserved"; 416 case 34: return "hflex"; 417 case 35: return "flex"; 418 case 36: return "hflex1"; 419 case 37: return "flex1"; 420 case 38: return "Reserved"; 421 default: return "Unknown"; 422 } 423 } 424 425 /** 426 * Validates the String index data and size 427 * @throws IOException 428 */ 429 @Test testStringIndex()430 public void testStringIndex() throws IOException { 431 CFFDataReader cffReaderSourceSans = getCFFReaderSourceSans(); 432 assertEquals(164, cffReaderSourceSans.getStringIndex().getNumObjects()); 433 assertTrue(checkCorrectOffsets(cffReaderSourceSans.getStringIndex())); 434 assertEquals("Amacron", new String(cffReaderSourceSans.getStringIndex().getValue(5))); 435 assertEquals("Edotaccent", new String(cffReaderSourceSans.getStringIndex().getValue(32))); 436 assertEquals("uni0122", new String(cffReaderSourceSans.getStringIndex().getValue(45))); 437 } 438 439 /** 440 * Validates the Top Dict data 441 * @throws IOException 442 */ 443 @Test testTopDictData()444 public void testTopDictData() throws IOException { 445 CFFDataReader cffReaderSourceSans = getCFFReaderSourceSans(); 446 Map<String, DICTEntry> topDictEntries = cffReaderSourceSans.parseDictData( 447 cffReaderSourceSans.getTopDictIndex().getData()); 448 assertEquals(10, topDictEntries.size()); 449 } 450 451 @Test testFDSelect()452 public void testFDSelect() throws IOException { 453 Assert.assertEquals(getSubset(1).length, 46); 454 Assert.assertEquals(getSubset(2).length, 45); 455 } 456 getSubset(final int opLen)457 private byte[] getSubset(final int opLen) throws IOException { 458 FontFileReader reader = sourceSansReader; 459 OTFSubSetFile otfSubSetFile = new MyOTFSubSetFile(opLen); 460 otfSubSetFile.readFont(reader, "StandardOpenType", null, new HashMap<Integer, Integer>()); 461 return otfSubSetFile.getFontSubset(); 462 } 463 464 @Test testOffsets()465 public void testOffsets() throws IOException { 466 StringBuilder sb = new StringBuilder(); 467 for (int i = 0; i < 2048; i++) { 468 sb.append("SourceSansProBold"); 469 } 470 OTFSubSetFile otfSubSetFile = new OTFSubSetFile(); 471 otfSubSetFile.readFont(sourceSansReader, sb.toString(), null, glyphs); 472 new CFFParser().parse(otfSubSetFile.getFontSubset()); 473 } 474 475 @Test testCharset()476 public void testCharset() throws IOException { 477 FontFileReader reader = sourceSansReader; 478 MyOTFSubSetFile otfSubSetFile = new MyOTFSubSetFile(1); 479 otfSubSetFile.readFont(reader, "StandardOpenType", null, new HashMap<Integer, Integer>()); 480 ByteArrayInputStream is = new ByteArrayInputStream(otfSubSetFile.getFontSubset()); 481 is.skip(otfSubSetFile.charsetOffset); 482 Assert.assertEquals(is.read(), 2); 483 } 484 485 class MyOTFSubSetFile extends OTFSubSetFile { 486 int charsetOffset; 487 int opLen; MyOTFSubSetFile(int opLen)488 MyOTFSubSetFile(int opLen) throws IOException { 489 super(); 490 this.opLen = opLen; 491 } 492 createCFF()493 protected void createCFF() throws IOException { 494 cffReader = mock(CFFDataReader.class); 495 when(cffReader.getHeader()).thenReturn(new byte[0]); 496 when(cffReader.getTopDictIndex()).thenReturn(new CFFDataReader().new CFFIndexData() { 497 public byte[] getByteData() throws IOException { 498 return new byte[] {0, 0, 1}; 499 } 500 }); 501 502 LinkedHashMap<String, DICTEntry> map = new LinkedHashMap<String, DICTEntry>(); 503 DICTEntry dict = new DICTEntry(); 504 dict.setOperands(Collections.<Number>singletonList(1)); 505 map.put("charset", dict); 506 map.put("CharStrings", dict); 507 when((cffReader.getTopDictEntries())).thenReturn(map); 508 CFFDataReader.Format3FDSelect fdSelect = new CFFDataReader().new Format3FDSelect(); 509 fdSelect.setRanges(new HashMap<Integer, Integer>()); 510 when(cffReader.getFDSelect()).thenReturn(fdSelect); 511 cffReader.getTopDictEntries().get("CharStrings").setOperandLength(opLen); 512 super.createCFF(); 513 } 514 updateFixedOffsets(Map<String, DICTEntry> topDICT, Offsets offsets)515 protected void updateFixedOffsets(Map<String, DICTEntry> topDICT, Offsets offsets) throws IOException { 516 this.charsetOffset = offsets.charset; 517 super.updateFixedOffsets(topDICT, offsets); 518 } 519 } 520 521 @Test testResizeOfOperand()522 public void testResizeOfOperand() throws IOException { 523 OTFSubSetFile otfSubSetFile = new OTFSubSetFile() { 524 protected void writeFDSelect() { 525 super.writeFDSelect(); 526 writeBytes(new byte[1024 * 100]); 527 } 528 }; 529 otfSubSetFile.readFont(sourceSansReader, "StandardOpenType", null, glyphs); 530 byte[] fontSubset = otfSubSetFile.getFontSubset(); 531 CFFDataReader cffReader = new CFFDataReader(fontSubset); 532 assertEquals(cffReader.getTopDictEntries().get("CharStrings").getOperandLength(), 5); 533 assertEquals(cffReader.getTopDictEntries().get("CharStrings").getByteData().length, 6); 534 } 535 536 @Test testFDArraySize()537 public void testFDArraySize() throws IOException { 538 OTFSubSetFileFDArraySize otfSubSetFileFDArraySize = new OTFSubSetFileFDArraySize(); 539 otfSubSetFileFDArraySize.readFont(sourceSansReader, "StandardOpenType", null, glyphs); 540 byte[] fontSubset = otfSubSetFileFDArraySize.getFontSubset(); 541 DataInputStream dis = new DataInputStream(new ByteArrayInputStream(fontSubset)); 542 dis.skipBytes(otfSubSetFileFDArraySize.offset); 543 Assert.assertEquals(dis.readUnsignedShort(), otfSubSetFileFDArraySize.fdFontCount); 544 Assert.assertEquals(dis.readByte(), 2); 545 } 546 547 static class OTFSubSetFileFDArraySize extends OTFSubSetFile { 548 int offset; 549 int fdFontCount = 128; 550 OTFSubSetFileFDArraySize()551 OTFSubSetFileFDArraySize() throws IOException { 552 super(); 553 } 554 createCFF()555 protected void createCFF() throws IOException { 556 super.createCFF(); 557 writeFDArray(new ArrayList<Integer>(), new ArrayList<Integer>(), new ArrayList<Integer>()); 558 } 559 writeFDArray(List<Integer> uniqueNewRefs, List<Integer> privateDictOffsets, List<Integer> fontNameSIDs)560 protected int writeFDArray(List<Integer> uniqueNewRefs, List<Integer> privateDictOffsets, 561 List<Integer> fontNameSIDs) throws IOException { 562 List<CFFDataReader.FontDict> fdFonts = cffReader.getFDFonts(); 563 CFFDataReader.FontDict fdFont = cffReader.new FontDict() { 564 public byte[] getByteData() throws IOException { 565 return new byte[128]; 566 } 567 }; 568 cffReader = makeCFFDataReader(); 569 when(cffReader.getFDFonts()).thenReturn(fdFonts); 570 571 fdFonts.clear(); 572 uniqueNewRefs.clear(); 573 privateDictOffsets.clear(); 574 fontNameSIDs.clear(); 575 for (int i = 0; i < fdFontCount; i++) { 576 fdFonts.add(fdFont); 577 uniqueNewRefs.add(i); 578 privateDictOffsets.add(i); 579 fontNameSIDs.add(i); 580 } 581 offset = super.writeFDArray(uniqueNewRefs, privateDictOffsets, fontNameSIDs); 582 return offset; 583 } 584 } 585 586 @Test testOrderOfEntries()587 public void testOrderOfEntries() throws IOException { 588 OTFSubSetFileEntryOrder otfSubSetFile = getFont(3, 2); 589 assertTrue(otfSubSetFile.offsets.fdArray < otfSubSetFile.offsets.charString); 590 assertEquals(otfSubSetFile.cffReader.getTopDictEntries().get("CharStrings").getOperandLength(), 5); 591 otfSubSetFile = getFont(2, 3); 592 assertTrue(otfSubSetFile.offsets.fdArray < otfSubSetFile.offsets.charString); 593 assertEquals(otfSubSetFile.cffReader.getTopDictEntries().get("CharStrings").getOperandLength(), 5); 594 } 595 596 private OTFSubSetFileEntryOrder getFont(int csLen, int fdLen) throws IOException { 597 glyphs.clear(); 598 OTFSubSetFileEntryOrder otfSubSetFile = new OTFSubSetFileEntryOrder(csLen, fdLen); 599 otfSubSetFile.readFont(sourceSansReader, "StandardOpenType", null, glyphs); 600 return otfSubSetFile; 601 } 602 603 static class OTFSubSetFileEntryOrder extends OTFSubSetFile { 604 Offsets offsets; 605 int csLen; 606 int fdLen; 607 608 OTFSubSetFileEntryOrder(int csLen, int fdLen) throws IOException { 609 super(); 610 this.csLen = csLen; 611 this.fdLen = fdLen; 612 } 613 614 protected void createCFF() throws IOException { 615 cffReader = makeCFFDataReader(); 616 LinkedHashMap<String, DICTEntry> topDict = new LinkedHashMap<String, DICTEntry>(); 617 DICTEntry entry = new DICTEntry(); 618 entry.setOperands(Collections.<Number>singletonList(0)); 619 topDict.put("charset", entry); 620 entry.setOperandLength(csLen); 621 topDict.put("CharStrings", entry); 622 entry = new DICTEntry(); 623 entry.setOperandLength(fdLen); 624 topDict.put("FDArray", entry); 625 when(cffReader.getTopDictEntries()).thenReturn(topDict); 626 super.createCFF(); 627 } 628 629 protected void updateCIDOffsets(Offsets offsets) throws IOException { 630 super.updateCIDOffsets(offsets); 631 this.offsets = offsets; 632 } 633 } 634 635 private static CFFDataReader makeCFFDataReader() throws IOException { 636 CFFDataReader cffReader = mock(CFFDataReader.class); 637 when(cffReader.getHeader()).thenReturn(new byte[0]); 638 when(cffReader.getTopDictIndex()).thenReturn(cffReader.new CFFIndexData() { 639 public byte[] getByteData() throws IOException { 640 return new byte[]{0, 0, 1}; 641 } 642 }); 643 CFFDataReader.Format3FDSelect fdSelect = cffReader.new Format3FDSelect(); 644 fdSelect.setRanges(new HashMap<Integer, Integer>()); 645 when(cffReader.getFDSelect()).thenReturn(fdSelect); 646 CFFDataReader.FontDict fd = mock(CFFDataReader.FontDict.class); 647 when(fd.getPrivateDictData()).thenReturn(new byte[0]); 648 when(cffReader.getFDFonts()).thenReturn(Collections.singletonList(fd)); 649 650 LinkedHashMap<String, DICTEntry> map = new LinkedHashMap<String, DICTEntry>(); 651 DICTEntry e = new DICTEntry(); 652 e.setOffset(1); 653 e.setOperandLengths(Arrays.asList(0, 0)); 654 e.setOperandLength(2); 655 map.put("FontName", e); 656 map.put("Private", e); 657 map.put("Subrs", e); 658 when(cffReader.parseDictData(any(byte[].class))).thenReturn(map); 659 return cffReader; 660 } 661 662 @Test 663 public void testWriteCIDDictsAndSubrs() throws IOException { 664 OTFSubSetFile subSetFile = new OTFSubSetFile() { 665 public void readFont(FontFileReader in, String embeddedName, MultiByteFont mbFont) throws IOException { 666 cffReader = makeCFFDataReader(); 667 fdSubrs = new ArrayList<List<byte[]>>(); 668 fdSubrs.add(new ArrayList<byte[]>()); 669 writeCIDDictsAndSubrs(Collections.singletonList(0)); 670 } 671 }; 672 subSetFile.readFont(null, null, (MultiByteFont) null); 673 674 ByteArrayInputStream is = new ByteArrayInputStream(subSetFile.getFontSubset()); 675 is.skip(1); 676 Assert.assertEquals(is.read(), 247); 677 Assert.assertEquals(is.read(), 0); 678 final int sizeOfPrivateDictByteData = 108; 679 is.skip(sizeOfPrivateDictByteData - 3); 680 is.skip(2); //start index 681 Assert.assertEquals(is.read(), 1); 682 } 683 684 @Test 685 public void testResizeOfOperand2() throws IOException { 686 OTFSubSetFile otfSubSetFile = new OTFSubSetFile() { 687 void readFont(FontFileReader in, String embeddedName, MultiByteFont mbFont, 688 Map<Integer, Integer> usedGlyphs) throws IOException { 689 cffReader = makeCFFDataReader(); 690 LinkedHashMap<String, DICTEntry> topDict = new LinkedHashMap<String, DICTEntry>(); 691 DICTEntry entry = new DICTEntry(); 692 entry.setOperandLength(1); 693 entry.setOperator(new int[0]); 694 entry.setOperands(Collections.<Number>singletonList(0)); 695 topDict.put("version", entry); 696 when(cffReader.getTopDictEntries()).thenReturn(topDict); 697 writeTopDICT(); 698 } 699 }; 700 otfSubSetFile.readFont(sourceSansReader, "StandardOpenType", null, glyphs); 701 ByteArrayInputStream fontSubset = new ByteArrayInputStream(otfSubSetFile.getFontSubset()); 702 fontSubset.skip(5); 703 Assert.assertEquals(fontSubset.read(), 248); 704 Assert.assertEquals(fontSubset.read(), (byte)(390 - 108)); 705 } 706 707 @Test 708 public void testCompositeGlyphMapping() throws IOException { 709 glyphs.clear(); 710 glyphs.put(0, 0); 711 OTFSubSetFile sourceSansSubset = new OTFSubSetFile() { 712 protected void initializeFont(FontFileReader in) { 713 fileFont = new CFFType1Font() { 714 List<Object> sequence = Arrays.asList(0, 0, 0, (int)'a', (int)'b', new CharStringCommand(12, 6)); 715 public Type2CharString getType2CharString(int gid) { 716 return new Type2CharString(null, null, null, 0, sequence, 0, 0); 717 } 718 }; 719 } 720 }; 721 MultiByteFont multiByteFont = new MultiByteFont(null, null) { 722 public void setEmbedResourceName(String name) { 723 super.setEmbedResourceName(name); 724 addPrivateUseMapping('a', 'a'); 725 addPrivateUseMapping('b', 'b'); 726 } 727 }; 728 multiByteFont.setEmbedURI(new File(".").toURI()); 729 multiByteFont.setEmbedResourceName(""); 730 sourceSansSubset.readFont(sourceSansReader, "SourceSansProBold", multiByteFont, glyphs); 731 Assert.assertEquals(multiByteFont.getUsedGlyphs().toString(), "{0=0, 97=1, 98=2}"); 732 } 733 } 734