You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

EscherDggRecord.java 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  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. http://www.apache.org/licenses/LICENSE-2.0
  9. Unless required by applicable law or agreed to in writing, software
  10. distributed under the License is distributed on an "AS IS" BASIS,
  11. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. See the License for the specific language governing permissions and
  13. limitations under the License.
  14. ==================================================================== */
  15. package org.apache.poi.ddf;
  16. import java.util.ArrayList;
  17. import java.util.Arrays;
  18. import java.util.List;
  19. import java.util.Map;
  20. import java.util.function.Supplier;
  21. import com.zaxxer.sparsebits.SparseBitSet;
  22. import org.apache.poi.common.usermodel.GenericRecord;
  23. import org.apache.poi.util.GenericRecordUtil;
  24. import org.apache.poi.util.LittleEndian;
  25. import org.apache.poi.util.RecordFormatException;
  26. /**
  27. * This record defines the drawing groups used for a particular sheet.
  28. */
  29. public final class EscherDggRecord extends EscherRecord {
  30. public static final short RECORD_ID = EscherRecordTypes.DGG.typeID;
  31. private int field_1_shapeIdMax;
  32. // for some reason the number of clusters is actually the real number + 1
  33. // private int field_2_numIdClusters;
  34. private int field_3_numShapesSaved;
  35. private int field_4_drawingsSaved;
  36. private final List<FileIdCluster> field_5_fileIdClusters = new ArrayList<>();
  37. private int maxDgId;
  38. public static class FileIdCluster implements GenericRecord {
  39. private int field_1_drawingGroupId;
  40. private int field_2_numShapeIdsUsed;
  41. public FileIdCluster(FileIdCluster other) {
  42. field_1_drawingGroupId = other.field_1_drawingGroupId;
  43. field_2_numShapeIdsUsed = other.field_2_numShapeIdsUsed;
  44. }
  45. public FileIdCluster( int drawingGroupId, int numShapeIdsUsed ) {
  46. this.field_1_drawingGroupId = drawingGroupId;
  47. this.field_2_numShapeIdsUsed = numShapeIdsUsed;
  48. }
  49. public int getDrawingGroupId() {
  50. return field_1_drawingGroupId;
  51. }
  52. public int getNumShapeIdsUsed() {
  53. return field_2_numShapeIdsUsed;
  54. }
  55. private void incrementUsedShapeId() {
  56. field_2_numShapeIdsUsed++;
  57. }
  58. private static int compareFileIdCluster(FileIdCluster f1, FileIdCluster f2) {
  59. int dgDif = f1.getDrawingGroupId() - f2.getDrawingGroupId();
  60. int cntDif = f2.getNumShapeIdsUsed() - f1.getNumShapeIdsUsed();
  61. return (dgDif != 0) ? dgDif : cntDif;
  62. }
  63. @Override
  64. public Map<String, Supplier<?>> getGenericProperties() {
  65. return GenericRecordUtil.getGenericProperties(
  66. "drawingGroupId", this::getDrawingGroupId,
  67. "numShapeIdUsed", this::getNumShapeIdsUsed
  68. );
  69. }
  70. }
  71. public EscherDggRecord() {}
  72. public EscherDggRecord(EscherDggRecord other) {
  73. super(other);
  74. field_1_shapeIdMax = other.field_1_shapeIdMax;
  75. field_3_numShapesSaved = other.field_3_numShapesSaved;
  76. field_4_drawingsSaved = other.field_4_drawingsSaved;
  77. other.field_5_fileIdClusters.stream().map(FileIdCluster::new).forEach(field_5_fileIdClusters::add);
  78. maxDgId = other.maxDgId;
  79. }
  80. @Override
  81. public int fillFields(byte[] data, int offset, EscherRecordFactory recordFactory) {
  82. int bytesRemaining = readHeader( data, offset );
  83. int pos = offset + 8;
  84. int size = 0;
  85. field_1_shapeIdMax = LittleEndian.getInt( data, pos + size );size+=4;
  86. // field_2_numIdClusters = LittleEndian.getInt( data, pos + size );
  87. size+=4;
  88. field_3_numShapesSaved = LittleEndian.getInt( data, pos + size );size+=4;
  89. field_4_drawingsSaved = LittleEndian.getInt( data, pos + size );size+=4;
  90. field_5_fileIdClusters.clear();
  91. // Can't rely on field_2_numIdClusters
  92. int numIdClusters = (bytesRemaining-size) / 8;
  93. for (int i = 0; i < numIdClusters; i++) {
  94. int drawingGroupId = LittleEndian.getInt( data, pos + size );
  95. int numShapeIdsUsed = LittleEndian.getInt( data, pos + size + 4 );
  96. FileIdCluster fic = new FileIdCluster(drawingGroupId, numShapeIdsUsed);
  97. field_5_fileIdClusters.add(fic);
  98. maxDgId = Math.max(maxDgId, drawingGroupId);
  99. size += 8;
  100. }
  101. bytesRemaining -= size;
  102. if (bytesRemaining != 0) {
  103. throw new RecordFormatException("Expecting no remaining data but got " + bytesRemaining + " byte(s).");
  104. }
  105. return 8 + size;
  106. }
  107. @Override
  108. public int serialize(int offset, byte[] data, EscherSerializationListener listener) {
  109. listener.beforeRecordSerialize( offset, getRecordId(), this );
  110. int pos = offset;
  111. LittleEndian.putShort( data, pos, getOptions() ); pos += 2;
  112. LittleEndian.putShort( data, pos, getRecordId() ); pos += 2;
  113. int remainingBytes = getRecordSize() - 8;
  114. LittleEndian.putInt( data, pos, remainingBytes ); pos += 4;
  115. LittleEndian.putInt( data, pos, field_1_shapeIdMax ); pos += 4;
  116. LittleEndian.putInt( data, pos, getNumIdClusters() ); pos += 4;
  117. LittleEndian.putInt( data, pos, field_3_numShapesSaved ); pos += 4;
  118. LittleEndian.putInt( data, pos, field_4_drawingsSaved ); pos += 4;
  119. for (FileIdCluster fic : field_5_fileIdClusters) {
  120. LittleEndian.putInt( data, pos, fic.getDrawingGroupId() ); pos += 4;
  121. LittleEndian.putInt( data, pos, fic.getNumShapeIdsUsed() ); pos += 4;
  122. }
  123. listener.afterRecordSerialize( pos, getRecordId(), getRecordSize(), this );
  124. return getRecordSize();
  125. }
  126. @Override
  127. public int getRecordSize() {
  128. return 8 + 16 + (8 * field_5_fileIdClusters.size());
  129. }
  130. @Override
  131. public short getRecordId() {
  132. return RECORD_ID;
  133. }
  134. @Override
  135. public String getRecordName() {
  136. return EscherRecordTypes.DGG.recordName;
  137. }
  138. /**
  139. * Gets the next available shape id
  140. *
  141. * @return the next available shape id
  142. */
  143. public int getShapeIdMax() {
  144. return field_1_shapeIdMax;
  145. }
  146. /**
  147. * The maximum is actually the next available shape id.
  148. *
  149. * @param shapeIdMax the next available shape id
  150. */
  151. public void setShapeIdMax(int shapeIdMax) {
  152. this.field_1_shapeIdMax = shapeIdMax;
  153. }
  154. /**
  155. * Number of id clusters + 1
  156. *
  157. * @return the number of id clusters + 1
  158. */
  159. public int getNumIdClusters() {
  160. return (field_5_fileIdClusters.isEmpty() ? 0 : field_5_fileIdClusters.size() + 1);
  161. }
  162. /**
  163. * Gets the number of shapes saved
  164. *
  165. * @return the number of shapes saved
  166. */
  167. public int getNumShapesSaved() {
  168. return field_3_numShapesSaved;
  169. }
  170. /**
  171. * Sets the number of shapes saved
  172. *
  173. * @param numShapesSaved the number of shapes saved
  174. */
  175. public void setNumShapesSaved(int numShapesSaved) {
  176. this.field_3_numShapesSaved = numShapesSaved;
  177. }
  178. /**
  179. * Gets the number of drawings saved
  180. *
  181. * @return the number of drawings saved
  182. */
  183. public int getDrawingsSaved() {
  184. return field_4_drawingsSaved;
  185. }
  186. /**
  187. * Sets the number of drawings saved
  188. *
  189. * @param drawingsSaved the number of drawings saved
  190. */
  191. public void setDrawingsSaved(int drawingsSaved) {
  192. this.field_4_drawingsSaved = drawingsSaved;
  193. }
  194. /**
  195. * Gets the maximum drawing group ID
  196. *
  197. * @return The maximum drawing group ID
  198. */
  199. public int getMaxDrawingGroupId() {
  200. return maxDgId;
  201. }
  202. /**
  203. * @return the file id clusters
  204. */
  205. public FileIdCluster[] getFileIdClusters() {
  206. return field_5_fileIdClusters.toArray(new FileIdCluster[0]);
  207. }
  208. /**
  209. * Sets the file id clusters
  210. *
  211. * @param fileIdClusters the file id clusters
  212. */
  213. public void setFileIdClusters(FileIdCluster[] fileIdClusters) {
  214. field_5_fileIdClusters.clear();
  215. if (fileIdClusters != null) {
  216. field_5_fileIdClusters.addAll(Arrays.asList(fileIdClusters));
  217. }
  218. }
  219. /**
  220. * Add a new cluster
  221. *
  222. * @param dgId id of the drawing group (stored in the record options)
  223. * @param numShapedUsed initial value of the numShapedUsed field
  224. *
  225. * @return the new {@link FileIdCluster}
  226. */
  227. public FileIdCluster addCluster(int dgId, int numShapedUsed) {
  228. return addCluster(dgId, numShapedUsed, true);
  229. }
  230. /**
  231. * Add a new cluster
  232. *
  233. * @param dgId id of the drawing group (stored in the record options)
  234. * @param numShapedUsed initial value of the numShapedUsed field
  235. * @param sort if true then sort clusters by drawing group id.(
  236. * In Excel the clusters are sorted but in PPT they are not)
  237. *
  238. * @return the new {@link FileIdCluster}
  239. */
  240. public FileIdCluster addCluster( int dgId, int numShapedUsed, boolean sort ) {
  241. FileIdCluster ficNew = new FileIdCluster(dgId, numShapedUsed);
  242. field_5_fileIdClusters.add(ficNew);
  243. maxDgId = Math.min(maxDgId, dgId);
  244. if (sort) {
  245. sortCluster();
  246. }
  247. return ficNew;
  248. }
  249. private void sortCluster() {
  250. field_5_fileIdClusters.sort(FileIdCluster::compareFileIdCluster);
  251. }
  252. /**
  253. * Finds the next available (1 based) drawing group id
  254. *
  255. * @return the next available drawing group id
  256. */
  257. public short findNewDrawingGroupId() {
  258. SparseBitSet bs = new SparseBitSet();
  259. bs.set(0);
  260. for (FileIdCluster fic : field_5_fileIdClusters) {
  261. bs.set(fic.getDrawingGroupId());
  262. }
  263. return (short)bs.nextClearBit(0);
  264. }
  265. /**
  266. * Allocates new shape id for the drawing group
  267. *
  268. * @param dg the EscherDgRecord which receives the new shape
  269. * @param sort if true then sort clusters by drawing group id.(
  270. * In Excel the clusters are sorted but in PPT they are not)
  271. *
  272. * @return a new shape id.
  273. */
  274. public int allocateShapeId(EscherDgRecord dg, boolean sort) {
  275. final short drawingGroupId = dg.getDrawingGroupId();
  276. field_3_numShapesSaved++;
  277. // check for an existing cluster, which has space available
  278. // see 2.2.46 OfficeArtIDCL (cspidCur) for the 1024 limitation
  279. // multiple clusters can belong to the same drawing group
  280. FileIdCluster ficAdd = null;
  281. int index = 1;
  282. for (FileIdCluster fic : field_5_fileIdClusters) {
  283. if (fic.getDrawingGroupId() == drawingGroupId
  284. && fic.getNumShapeIdsUsed() < 1024) {
  285. ficAdd = fic;
  286. break;
  287. }
  288. index++;
  289. }
  290. if (ficAdd == null) {
  291. ficAdd = addCluster( drawingGroupId, 0, sort );
  292. maxDgId = Math.max(maxDgId, drawingGroupId);
  293. }
  294. int shapeId = index*1024 + ficAdd.getNumShapeIdsUsed();
  295. ficAdd.incrementUsedShapeId();
  296. dg.setNumShapes( dg.getNumShapes() + 1 );
  297. dg.setLastMSOSPID( shapeId );
  298. field_1_shapeIdMax = Math.max(field_1_shapeIdMax, shapeId + 1);
  299. return shapeId;
  300. }
  301. @Override
  302. public Enum getGenericRecordType() {
  303. return EscherRecordTypes.DGG;
  304. }
  305. @Override
  306. public Map<String, Supplier<?>> getGenericProperties() {
  307. return GenericRecordUtil.getGenericProperties(
  308. "base", super::getGenericProperties,
  309. "fileIdClusters", () -> field_5_fileIdClusters,
  310. "shapeIdMax", this::getShapeIdMax,
  311. "numIdClusters", this::getNumIdClusters,
  312. "numShapesSaved", this::getNumShapesSaved,
  313. "drawingsSaved", this::getDrawingsSaved
  314. );
  315. }
  316. @Override
  317. public EscherDggRecord copy() {
  318. return new EscherDggRecord(this);
  319. }
  320. }