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.

HSLFSlideShow.java 37KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122
  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.hslf.usermodel;
  16. import java.awt.Dimension;
  17. import java.io.ByteArrayInputStream;
  18. import java.io.ByteArrayOutputStream;
  19. import java.io.Closeable;
  20. import java.io.File;
  21. import java.io.FileInputStream;
  22. import java.io.IOException;
  23. import java.io.InputStream;
  24. import java.io.OutputStream;
  25. import java.util.ArrayList;
  26. import java.util.Arrays;
  27. import java.util.Collections;
  28. import java.util.HashMap;
  29. import java.util.List;
  30. import java.util.Map;
  31. import org.apache.poi.ddf.EscherBSERecord;
  32. import org.apache.poi.ddf.EscherContainerRecord;
  33. import org.apache.poi.ddf.EscherOptRecord;
  34. import org.apache.poi.hpsf.ClassID;
  35. import org.apache.poi.hslf.exceptions.CorruptPowerPointFileException;
  36. import org.apache.poi.hslf.exceptions.HSLFException;
  37. import org.apache.poi.hslf.model.HeadersFooters;
  38. import org.apache.poi.hslf.model.MovieShape;
  39. import org.apache.poi.hslf.model.PPFont;
  40. import org.apache.poi.hslf.record.Document;
  41. import org.apache.poi.hslf.record.DocumentAtom;
  42. import org.apache.poi.hslf.record.ExAviMovie;
  43. import org.apache.poi.hslf.record.ExControl;
  44. import org.apache.poi.hslf.record.ExEmbed;
  45. import org.apache.poi.hslf.record.ExEmbedAtom;
  46. import org.apache.poi.hslf.record.ExMCIMovie;
  47. import org.apache.poi.hslf.record.ExObjList;
  48. import org.apache.poi.hslf.record.ExObjListAtom;
  49. import org.apache.poi.hslf.record.ExOleObjAtom;
  50. import org.apache.poi.hslf.record.ExOleObjStg;
  51. import org.apache.poi.hslf.record.ExVideoContainer;
  52. import org.apache.poi.hslf.record.FontCollection;
  53. import org.apache.poi.hslf.record.FontEntityAtom;
  54. import org.apache.poi.hslf.record.HeadersFootersContainer;
  55. import org.apache.poi.hslf.record.MainMaster;
  56. import org.apache.poi.hslf.record.Notes;
  57. import org.apache.poi.hslf.record.PersistPtrHolder;
  58. import org.apache.poi.hslf.record.PositionDependentRecord;
  59. import org.apache.poi.hslf.record.PositionDependentRecordContainer;
  60. import org.apache.poi.hslf.record.Record;
  61. import org.apache.poi.hslf.record.RecordContainer;
  62. import org.apache.poi.hslf.record.RecordTypes;
  63. import org.apache.poi.hslf.record.Slide;
  64. import org.apache.poi.hslf.record.SlideListWithText;
  65. import org.apache.poi.hslf.record.SlideListWithText.SlideAtomsSet;
  66. import org.apache.poi.hslf.record.SlidePersistAtom;
  67. import org.apache.poi.hslf.record.TxMasterStyleAtom;
  68. import org.apache.poi.hslf.record.UserEditAtom;
  69. import org.apache.poi.poifs.filesystem.DirectoryNode;
  70. import org.apache.poi.poifs.filesystem.NPOIFSFileSystem;
  71. import org.apache.poi.poifs.filesystem.POIFSFileSystem;
  72. import org.apache.poi.sl.usermodel.MasterSheet;
  73. import org.apache.poi.sl.usermodel.PictureData.PictureType;
  74. import org.apache.poi.sl.usermodel.Resources;
  75. import org.apache.poi.sl.usermodel.SlideShow;
  76. import org.apache.poi.util.POILogFactory;
  77. import org.apache.poi.util.POILogger;
  78. import org.apache.poi.util.Units;
  79. /**
  80. * This class is a friendly wrapper on top of the more scary HSLFSlideShow.
  81. *
  82. * TODO: - figure out how to match notes to their correct sheet (will involve
  83. * understanding DocSlideList and DocNotesList) - handle Slide creation cleaner
  84. *
  85. * @author Nick Burch
  86. * @author Yegor kozlov
  87. */
  88. public final class HSLFSlideShow implements SlideShow<HSLFShape,HSLFTextParagraph>, Closeable {
  89. enum LoadSavePhase {
  90. INIT, LOADED
  91. }
  92. private static ThreadLocal<LoadSavePhase> loadSavePhase = new ThreadLocal<LoadSavePhase>();
  93. // What we're based on
  94. private HSLFSlideShowImpl _hslfSlideShow;
  95. // Pointers to the most recent versions of the core records
  96. // (Document, Notes, Slide etc)
  97. private Record[] _mostRecentCoreRecords;
  98. // Lookup between the PersitPtr "sheet" IDs, and the position
  99. // in the mostRecentCoreRecords array
  100. private Map<Integer,Integer> _sheetIdToCoreRecordsLookup;
  101. // Records that are interesting
  102. private Document _documentRecord;
  103. // Friendly objects for people to deal with
  104. private final List<HSLFSlideMaster> _masters = new ArrayList<HSLFSlideMaster>();
  105. private final List<HSLFTitleMaster> _titleMasters = new ArrayList<HSLFTitleMaster>();
  106. private final List<HSLFSlide> _slides = new ArrayList<HSLFSlide>();
  107. private final List<HSLFNotes> _notes = new ArrayList<HSLFNotes>();
  108. private FontCollection _fonts;
  109. // For logging
  110. private POILogger logger = POILogFactory.getLogger(this.getClass());
  111. /**
  112. * Constructs a Powerpoint document from the underlying
  113. * HSLFSlideShow object. Finds the model stuff from this
  114. *
  115. * @param hslfSlideShow the HSLFSlideShow to base on
  116. */
  117. public HSLFSlideShow(HSLFSlideShowImpl hslfSlideShow) {
  118. loadSavePhase.set(LoadSavePhase.INIT);
  119. // Get useful things from our base slideshow
  120. _hslfSlideShow = hslfSlideShow;
  121. // Handle Parent-aware Records
  122. for (Record record : _hslfSlideShow.getRecords()) {
  123. if(record instanceof RecordContainer){
  124. RecordContainer.handleParentAwareRecords((RecordContainer)record);
  125. }
  126. }
  127. // Find the versions of the core records we'll want to use
  128. findMostRecentCoreRecords();
  129. // Build up the model level Slides and Notes
  130. buildSlidesAndNotes();
  131. loadSavePhase.set(LoadSavePhase.LOADED);
  132. }
  133. /**
  134. * Constructs a new, empty, Powerpoint document.
  135. */
  136. public HSLFSlideShow() {
  137. this(HSLFSlideShowImpl.create());
  138. }
  139. /**
  140. * Constructs a Powerpoint document from an input stream.
  141. */
  142. @SuppressWarnings("resource")
  143. public HSLFSlideShow(InputStream inputStream) throws IOException {
  144. this(new HSLFSlideShowImpl(inputStream));
  145. }
  146. /**
  147. * Constructs a Powerpoint document from an POIFSFileSystem.
  148. */
  149. @SuppressWarnings("resource")
  150. public HSLFSlideShow(NPOIFSFileSystem npoifs) throws IOException {
  151. this(new HSLFSlideShowImpl(npoifs));
  152. }
  153. /**
  154. * Constructs a Powerpoint document from an DirectoryNode.
  155. */
  156. @SuppressWarnings("resource")
  157. public HSLFSlideShow(DirectoryNode root) throws IOException {
  158. this(new HSLFSlideShowImpl(root));
  159. }
  160. /**
  161. * @return the current loading/saving phase
  162. */
  163. protected static LoadSavePhase getLoadSavePhase() {
  164. return loadSavePhase.get();
  165. }
  166. /**
  167. * Use the PersistPtrHolder entries to figure out what is the "most recent"
  168. * version of all the core records (Document, Notes, Slide etc), and save a
  169. * record of them. Do this by walking from the oldest PersistPtr to the
  170. * newest, overwriting any references found along the way with newer ones
  171. */
  172. private void findMostRecentCoreRecords() {
  173. // To start with, find the most recent in the byte offset domain
  174. Map<Integer,Integer> mostRecentByBytes = new HashMap<Integer,Integer>();
  175. for (Record record : _hslfSlideShow.getRecords()) {
  176. if (record instanceof PersistPtrHolder) {
  177. PersistPtrHolder pph = (PersistPtrHolder) record;
  178. // If we've already seen any of the "slide" IDs for this
  179. // PersistPtr, remove their old positions
  180. int[] ids = pph.getKnownSlideIDs();
  181. for (int id : ids) {
  182. if (mostRecentByBytes.containsKey(id)) {
  183. mostRecentByBytes.remove(id);
  184. }
  185. }
  186. // Now, update the byte level locations with their latest values
  187. Map<Integer,Integer> thisSetOfLocations = pph.getSlideLocationsLookup();
  188. for (int id : ids) {
  189. mostRecentByBytes.put(id, thisSetOfLocations.get(id));
  190. }
  191. }
  192. }
  193. // We now know how many unique special records we have, so init
  194. // the array
  195. _mostRecentCoreRecords = new Record[mostRecentByBytes.size()];
  196. // We'll also want to be able to turn the slide IDs into a position
  197. // in this array
  198. _sheetIdToCoreRecordsLookup = new HashMap<Integer,Integer>();
  199. Integer[] allIDs = mostRecentByBytes.keySet().toArray(new Integer[mostRecentByBytes.size()]);
  200. Arrays.sort(allIDs);
  201. for (int i = 0; i < allIDs.length; i++) {
  202. _sheetIdToCoreRecordsLookup.put(allIDs[i], i);
  203. }
  204. Map<Integer,Integer> mostRecentByBytesRev = new HashMap<Integer,Integer>(mostRecentByBytes.size());
  205. for (Map.Entry<Integer,Integer> me : mostRecentByBytes.entrySet()) {
  206. mostRecentByBytesRev.put(me.getValue(), me.getKey());
  207. }
  208. // Now convert the byte offsets back into record offsets
  209. for (Record record : _hslfSlideShow.getRecords()) {
  210. if (!(record instanceof PositionDependentRecord)) continue;
  211. PositionDependentRecord pdr = (PositionDependentRecord) record;
  212. int recordAt = pdr.getLastOnDiskOffset();
  213. Integer thisID = mostRecentByBytesRev.get(recordAt);
  214. if (thisID == null) continue;
  215. // Bingo. Now, where do we store it?
  216. int storeAt = _sheetIdToCoreRecordsLookup.get(thisID);
  217. // Tell it its Sheet ID, if it cares
  218. if (pdr instanceof PositionDependentRecordContainer) {
  219. PositionDependentRecordContainer pdrc = (PositionDependentRecordContainer) record;
  220. pdrc.setSheetId(thisID);
  221. }
  222. // Finally, save the record
  223. _mostRecentCoreRecords[storeAt] = record;
  224. }
  225. // Now look for the interesting records in there
  226. for (Record record : _mostRecentCoreRecords) {
  227. // Check there really is a record at this number
  228. if (record != null) {
  229. // Find the Document, and interesting things in it
  230. if (record.getRecordType() == RecordTypes.Document.typeID) {
  231. _documentRecord = (Document) record;
  232. _fonts = _documentRecord.getEnvironment().getFontCollection();
  233. }
  234. } else {
  235. // No record at this number
  236. // Odd, but not normally a problem
  237. }
  238. }
  239. }
  240. /**
  241. * For a given SlideAtomsSet, return the core record, based on the refID
  242. * from the SlidePersistAtom
  243. */
  244. private Record getCoreRecordForSAS(SlideAtomsSet sas) {
  245. SlidePersistAtom spa = sas.getSlidePersistAtom();
  246. int refID = spa.getRefID();
  247. return getCoreRecordForRefID(refID);
  248. }
  249. /**
  250. * For a given refID (the internal, 0 based numbering scheme), return the
  251. * core record
  252. *
  253. * @param refID
  254. * the refID
  255. */
  256. private Record getCoreRecordForRefID(int refID) {
  257. Integer coreRecordId = _sheetIdToCoreRecordsLookup.get(refID);
  258. if (coreRecordId != null) {
  259. Record r = _mostRecentCoreRecords[coreRecordId];
  260. return r;
  261. }
  262. logger.log(POILogger.ERROR,
  263. "We tried to look up a reference to a core record, but there was no core ID for reference ID "
  264. + refID);
  265. return null;
  266. }
  267. /**
  268. * Build up model level Slide and Notes objects, from the underlying
  269. * records.
  270. */
  271. private void buildSlidesAndNotes() {
  272. // Ensure we really found a Document record earlier
  273. // If we didn't, then the file is probably corrupt
  274. if (_documentRecord == null) {
  275. throw new CorruptPowerPointFileException(
  276. "The PowerPoint file didn't contain a Document Record in its PersistPtr blocks. It is probably corrupt.");
  277. }
  278. // Fetch the SlideListWithTexts in the most up-to-date Document Record
  279. //
  280. // As far as we understand it:
  281. // * The first SlideListWithText will contain a SlideAtomsSet
  282. // for each of the master slides
  283. // * The second SlideListWithText will contain a SlideAtomsSet
  284. // for each of the slides, in their current order
  285. // These SlideAtomsSets will normally contain text
  286. // * The third SlideListWithText (if present), will contain a
  287. // SlideAtomsSet for each Notes
  288. // These SlideAtomsSets will not normally contain text
  289. //
  290. // Having indentified the masters, slides and notes + their orders,
  291. // we have to go and find their matching records
  292. // We always use the latest versions of these records, and use the
  293. // SlideAtom/NotesAtom to match them with the StyleAtomSet
  294. findMasterSlides();
  295. // Having sorted out the masters, that leaves the notes and slides
  296. Map<Integer,Integer> slideIdToNotes = new HashMap<Integer,Integer>();
  297. // Start by finding the notes records
  298. findNotesSlides(slideIdToNotes);
  299. // Now, do the same thing for our slides
  300. findSlides(slideIdToNotes);
  301. }
  302. /**
  303. * Find master slides
  304. * These can be MainMaster records, but oddly they can also be
  305. * Slides or Notes, and possibly even other odd stuff....
  306. * About the only thing you can say is that the master details are in the first SLWT.
  307. */
  308. private void findMasterSlides() {
  309. SlideListWithText masterSLWT = _documentRecord.getMasterSlideListWithText();
  310. if (masterSLWT == null) {
  311. return;
  312. }
  313. for (SlideAtomsSet sas : masterSLWT.getSlideAtomsSets()) {
  314. Record r = getCoreRecordForSAS(sas);
  315. int sheetNo = sas.getSlidePersistAtom().getSlideIdentifier();
  316. if (r instanceof Slide) {
  317. HSLFTitleMaster master = new HSLFTitleMaster((Slide)r, sheetNo);
  318. master.setSlideShow(this);
  319. _titleMasters.add(master);
  320. } else if (r instanceof MainMaster) {
  321. HSLFSlideMaster master = new HSLFSlideMaster((MainMaster)r, sheetNo);
  322. master.setSlideShow(this);
  323. _masters.add(master);
  324. }
  325. }
  326. }
  327. private void findNotesSlides(Map<Integer,Integer> slideIdToNotes) {
  328. SlideListWithText notesSLWT = _documentRecord.getNotesSlideListWithText();
  329. if (notesSLWT == null) {
  330. return;
  331. }
  332. // Match up the records and the SlideAtomSets
  333. int idx = -1;
  334. for (SlideAtomsSet notesSet : notesSLWT.getSlideAtomsSets()) {
  335. idx++;
  336. // Get the right core record
  337. Record r = getCoreRecordForSAS(notesSet);
  338. SlidePersistAtom spa = notesSet.getSlidePersistAtom();
  339. String loggerLoc = "A Notes SlideAtomSet at "+idx+" said its record was at refID "+spa.getRefID();
  340. // we need to add null-records, otherwise the index references to other existing don't work anymore
  341. if (r == null) {
  342. logger.log(POILogger.WARN, loggerLoc+", but that record didn't exist - record ignored.");
  343. continue;
  344. }
  345. // Ensure it really is a notes record
  346. if (!(r instanceof Notes)) {
  347. logger.log(POILogger.ERROR, loggerLoc+", but that was actually a " + r);
  348. continue;
  349. }
  350. Notes notesRecord = (Notes) r;
  351. // Record the match between slide id and these notes
  352. int slideId = spa.getSlideIdentifier();
  353. slideIdToNotes.put(slideId, idx);
  354. HSLFNotes hn = new HSLFNotes(notesRecord);
  355. hn.setSlideShow(this);
  356. _notes.add(hn);
  357. }
  358. }
  359. private void findSlides(Map<Integer,Integer> slideIdToNotes) {
  360. SlideListWithText slidesSLWT = _documentRecord.getSlideSlideListWithText();
  361. if (slidesSLWT == null) {
  362. return;
  363. }
  364. // Match up the records and the SlideAtomSets
  365. int idx = -1;
  366. for (SlideAtomsSet sas : slidesSLWT.getSlideAtomsSets()) {
  367. idx++;
  368. // Get the right core record
  369. SlidePersistAtom spa = sas.getSlidePersistAtom();
  370. Record r = getCoreRecordForSAS(sas);
  371. // Ensure it really is a slide record
  372. if (!(r instanceof Slide)) {
  373. logger.log(POILogger.ERROR, "A Slide SlideAtomSet at " + idx
  374. + " said its record was at refID "
  375. + spa.getRefID()
  376. + ", but that was actually a " + r);
  377. continue;
  378. }
  379. Slide slide = (Slide)r;
  380. // Do we have a notes for this?
  381. HSLFNotes notes = null;
  382. // Slide.SlideAtom.notesId references the corresponding notes slide.
  383. // 0 if slide has no notes.
  384. int noteId = slide.getSlideAtom().getNotesID();
  385. if (noteId != 0) {
  386. Integer notesPos = slideIdToNotes.get(noteId);
  387. if (notesPos != null && 0 <= notesPos && notesPos < _notes.size()) {
  388. notes = _notes.get(notesPos);
  389. } else {
  390. logger.log(POILogger.ERROR, "Notes not found for noteId=" + noteId);
  391. }
  392. }
  393. // Now, build our slide
  394. int slideIdentifier = spa.getSlideIdentifier();
  395. HSLFSlide hs = new HSLFSlide(slide, notes, sas, slideIdentifier, (idx + 1));
  396. hs.setSlideShow(this);
  397. _slides.add(hs);
  398. }
  399. }
  400. @Override
  401. public void write(OutputStream out) throws IOException {
  402. // check for text paragraph modifications
  403. for (HSLFSlide sl : getSlides()) {
  404. writeDirtyParagraphs(sl);
  405. }
  406. for (HSLFSlideMaster sl : getSlideMasters()) {
  407. boolean isDirty = false;
  408. for (List<HSLFTextParagraph> paras : sl.getTextParagraphs()) {
  409. for (HSLFTextParagraph p : paras) {
  410. isDirty |= p.isDirty();
  411. }
  412. }
  413. if (isDirty) {
  414. for (TxMasterStyleAtom sa : sl.getTxMasterStyleAtoms()) {
  415. if (sa != null) {
  416. // not all master style atoms are set - index 3 is typically null
  417. sa.updateStyles();
  418. }
  419. }
  420. }
  421. }
  422. _hslfSlideShow.write(out);
  423. }
  424. private void writeDirtyParagraphs(HSLFShapeContainer container) {
  425. for (HSLFShape sh : container.getShapes()) {
  426. if (sh instanceof HSLFShapeContainer) {
  427. writeDirtyParagraphs((HSLFShapeContainer)sh);
  428. } else if (sh instanceof HSLFTextShape) {
  429. HSLFTextShape hts = (HSLFTextShape)sh;
  430. boolean isDirty = false;
  431. for (HSLFTextParagraph p : hts.getTextParagraphs()) {
  432. isDirty |= p.isDirty();
  433. }
  434. if (isDirty) hts.storeText();
  435. }
  436. }
  437. }
  438. /**
  439. * Returns an array of the most recent version of all the interesting
  440. * records
  441. */
  442. public Record[] getMostRecentCoreRecords() {
  443. return _mostRecentCoreRecords;
  444. }
  445. /**
  446. * Returns an array of all the normal Slides found in the slideshow
  447. */
  448. @Override
  449. public List<HSLFSlide> getSlides() {
  450. return _slides;
  451. }
  452. /**
  453. * Returns an array of all the normal Notes found in the slideshow
  454. */
  455. public List<HSLFNotes> getNotes() {
  456. return _notes;
  457. }
  458. /**
  459. * Returns an array of all the normal Slide Masters found in the slideshow
  460. */
  461. @Override
  462. public List<HSLFSlideMaster> getSlideMasters() {
  463. return _masters;
  464. }
  465. /**
  466. * Returns an array of all the normal Title Masters found in the slideshow
  467. */
  468. public List<HSLFTitleMaster> getTitleMasters() {
  469. return _titleMasters;
  470. }
  471. @Override
  472. public List<HSLFPictureData> getPictureData() {
  473. return _hslfSlideShow.getPictureData();
  474. }
  475. /**
  476. * Returns the data of all the embedded OLE object in the SlideShow
  477. */
  478. public HSLFObjectData[] getEmbeddedObjects() {
  479. return _hslfSlideShow.getEmbeddedObjects();
  480. }
  481. /**
  482. * Returns the data of all the embedded sounds in the SlideShow
  483. */
  484. public HSLFSoundData[] getSoundData() {
  485. return HSLFSoundData.find(_documentRecord);
  486. }
  487. @Override
  488. public Dimension getPageSize() {
  489. DocumentAtom docatom = _documentRecord.getDocumentAtom();
  490. int pgx = (int)Units.masterToPoints((int)docatom.getSlideSizeX());
  491. int pgy = (int)Units.masterToPoints((int)docatom.getSlideSizeY());
  492. return new Dimension(pgx, pgy);
  493. }
  494. @Override
  495. public void setPageSize(Dimension pgsize) {
  496. DocumentAtom docatom = _documentRecord.getDocumentAtom();
  497. docatom.setSlideSizeX(Units.pointsToMaster(pgsize.width));
  498. docatom.setSlideSizeY(Units.pointsToMaster(pgsize.height));
  499. }
  500. /**
  501. * Helper method for usermodel: Get the font collection
  502. */
  503. protected FontCollection getFontCollection() {
  504. return _fonts;
  505. }
  506. /**
  507. * Helper method for usermodel and model: Get the document record
  508. */
  509. public Document getDocumentRecord() {
  510. return _documentRecord;
  511. }
  512. /**
  513. * Re-orders a slide, to a new position.
  514. *
  515. * @param oldSlideNumber
  516. * The old slide number (1 based)
  517. * @param newSlideNumber
  518. * The new slide number (1 based)
  519. */
  520. public void reorderSlide(int oldSlideNumber, int newSlideNumber) {
  521. // Ensure these numbers are valid
  522. if (oldSlideNumber < 1 || newSlideNumber < 1) {
  523. throw new IllegalArgumentException("Old and new slide numbers must be greater than 0");
  524. }
  525. if (oldSlideNumber > _slides.size() || newSlideNumber > _slides.size()) {
  526. throw new IllegalArgumentException(
  527. "Old and new slide numbers must not exceed the number of slides ("
  528. + _slides.size() + ")");
  529. }
  530. // The order of slides is defined by the order of slide atom sets in the
  531. // SlideListWithText container.
  532. SlideListWithText slwt = _documentRecord.getSlideSlideListWithText();
  533. SlideAtomsSet[] sas = slwt.getSlideAtomsSets();
  534. SlideAtomsSet tmp = sas[oldSlideNumber - 1];
  535. sas[oldSlideNumber - 1] = sas[newSlideNumber - 1];
  536. sas[newSlideNumber - 1] = tmp;
  537. Collections.swap(_slides, oldSlideNumber - 1, newSlideNumber - 1);
  538. _slides.get(newSlideNumber - 1).setSlideNumber(newSlideNumber);
  539. _slides.get(oldSlideNumber - 1).setSlideNumber(oldSlideNumber);
  540. ArrayList<Record> lst = new ArrayList<Record>();
  541. for (SlideAtomsSet s : sas) {
  542. lst.add(s.getSlidePersistAtom());
  543. lst.addAll(Arrays.asList(s.getSlideRecords()));
  544. }
  545. Record[] r = lst.toArray(new Record[lst.size()]);
  546. slwt.setChildRecord(r);
  547. }
  548. /**
  549. * Removes the slide at the given index (0-based).
  550. * <p>
  551. * Shifts any subsequent slides to the left (subtracts one from their slide
  552. * numbers).
  553. * </p>
  554. *
  555. * @param index
  556. * the index of the slide to remove (0-based)
  557. * @return the slide that was removed from the slide show.
  558. */
  559. public HSLFSlide removeSlide(int index) {
  560. int lastSlideIdx = _slides.size() - 1;
  561. if (index < 0 || index > lastSlideIdx) {
  562. throw new IllegalArgumentException("Slide index (" + index + ") is out of range (0.."
  563. + lastSlideIdx + ")");
  564. }
  565. SlideListWithText slwt = _documentRecord.getSlideSlideListWithText();
  566. SlideAtomsSet[] sas = slwt.getSlideAtomsSets();
  567. List<Record> records = new ArrayList<Record>();
  568. List<SlideAtomsSet> sa = new ArrayList<SlideAtomsSet>(Arrays.asList(sas));
  569. HSLFSlide removedSlide = _slides.remove(index);
  570. _notes.remove(removedSlide.getNotes());
  571. sa.remove(index);
  572. int i=0;
  573. for (HSLFSlide s : _slides) s.setSlideNumber(i++);
  574. for (SlideAtomsSet s : sa) {
  575. records.add(s.getSlidePersistAtom());
  576. records.addAll(Arrays.asList(s.getSlideRecords()));
  577. }
  578. if (sa.isEmpty()) {
  579. _documentRecord.removeSlideListWithText(slwt);
  580. } else {
  581. slwt.setSlideAtomsSets(sa.toArray(new SlideAtomsSet[sa.size()]));
  582. slwt.setChildRecord(records.toArray(new Record[records.size()]));
  583. }
  584. // if the removed slide had notes - remove references to them too
  585. int notesId = removedSlide.getSlideRecord().getSlideAtom().getNotesID();
  586. if (notesId != 0) {
  587. SlideListWithText nslwt = _documentRecord.getNotesSlideListWithText();
  588. records = new ArrayList<Record>();
  589. ArrayList<SlideAtomsSet> na = new ArrayList<SlideAtomsSet>();
  590. for (SlideAtomsSet ns : nslwt.getSlideAtomsSets()) {
  591. if (ns.getSlidePersistAtom().getSlideIdentifier() == notesId) continue;
  592. na.add(ns);
  593. records.add(ns.getSlidePersistAtom());
  594. if (ns.getSlideRecords() != null) {
  595. records.addAll(Arrays.asList(ns.getSlideRecords()));
  596. }
  597. }
  598. if (na.isEmpty()) {
  599. _documentRecord.removeSlideListWithText(nslwt);
  600. } else {
  601. nslwt.setSlideAtomsSets(na.toArray(new SlideAtomsSet[na.size()]));
  602. nslwt.setChildRecord(records.toArray(new Record[records.size()]));
  603. }
  604. }
  605. return removedSlide;
  606. }
  607. /**
  608. * Create a blank <code>Slide</code>.
  609. *
  610. * @return the created <code>Slide</code>
  611. */
  612. @Override
  613. public HSLFSlide createSlide() {
  614. SlideListWithText slist = null;
  615. // We need to add the records to the SLWT that deals
  616. // with Slides.
  617. // Add it, if it doesn't exist
  618. slist = _documentRecord.getSlideSlideListWithText();
  619. if (slist == null) {
  620. // Need to add a new one
  621. slist = new SlideListWithText();
  622. slist.setInstance(SlideListWithText.SLIDES);
  623. _documentRecord.addSlideListWithText(slist);
  624. }
  625. // Grab the SlidePersistAtom with the highest Slide Number.
  626. // (Will stay as null if no SlidePersistAtom exists yet in
  627. // the slide, or only master slide's ones do)
  628. SlidePersistAtom prev = null;
  629. for (SlideAtomsSet sas : slist.getSlideAtomsSets()) {
  630. SlidePersistAtom spa = sas.getSlidePersistAtom();
  631. if (spa.getSlideIdentifier() < 0) {
  632. // This is for a master slide
  633. // Odd, since we only deal with the Slide SLWT
  634. } else {
  635. // Must be for a real slide
  636. if (prev == null) {
  637. prev = spa;
  638. }
  639. if (prev.getSlideIdentifier() < spa.getSlideIdentifier()) {
  640. prev = spa;
  641. }
  642. }
  643. }
  644. // Set up a new SlidePersistAtom for this slide
  645. SlidePersistAtom sp = new SlidePersistAtom();
  646. // First slideId is always 256
  647. sp.setSlideIdentifier(prev == null ? 256 : (prev.getSlideIdentifier() + 1));
  648. // Add this new SlidePersistAtom to the SlideListWithText
  649. slist.addSlidePersistAtom(sp);
  650. // Create a new Slide
  651. HSLFSlide slide = new HSLFSlide(sp.getSlideIdentifier(), sp.getRefID(), _slides.size() + 1);
  652. slide.setSlideShow(this);
  653. slide.onCreate();
  654. // Add in to the list of Slides
  655. _slides.add(slide);
  656. logger.log(POILogger.INFO, "Added slide " + _slides.size() + " with ref " + sp.getRefID()
  657. + " and identifier " + sp.getSlideIdentifier());
  658. // Add the core records for this new Slide to the record tree
  659. Slide slideRecord = slide.getSlideRecord();
  660. int psrId = addPersistentObject(slideRecord);
  661. sp.setRefID(psrId);
  662. slideRecord.setSheetId(psrId);
  663. slide.setMasterSheet(_masters.get(0));
  664. // All done and added
  665. return slide;
  666. }
  667. @Override
  668. public HSLFPictureData addPicture(byte[] data, PictureType format) throws IOException {
  669. if (format == null || format.nativeId == -1) {
  670. throw new IllegalArgumentException("Unsupported picture format: " + format);
  671. }
  672. byte[] uid = HSLFPictureData.getChecksum(data);
  673. for (HSLFPictureData pd : getPictureData()) {
  674. if (Arrays.equals(pd.getUID(), uid)) {
  675. return pd;
  676. }
  677. }
  678. EscherContainerRecord bstore;
  679. EscherContainerRecord dggContainer = _documentRecord.getPPDrawingGroup().getDggContainer();
  680. bstore = (EscherContainerRecord) HSLFShape.getEscherChild(dggContainer,
  681. EscherContainerRecord.BSTORE_CONTAINER);
  682. if (bstore == null) {
  683. bstore = new EscherContainerRecord();
  684. bstore.setRecordId(EscherContainerRecord.BSTORE_CONTAINER);
  685. dggContainer.addChildBefore(bstore, EscherOptRecord.RECORD_ID);
  686. }
  687. HSLFPictureData pict = HSLFPictureData.create(format);
  688. pict.setData(data);
  689. int offset = _hslfSlideShow.addPicture(pict);
  690. EscherBSERecord bse = new EscherBSERecord();
  691. bse.setRecordId(EscherBSERecord.RECORD_ID);
  692. bse.setOptions((short) (0x0002 | (format.nativeId << 4)));
  693. bse.setSize(pict.getRawData().length + 8);
  694. bse.setUid(uid);
  695. bse.setBlipTypeMacOS((byte) format.nativeId);
  696. bse.setBlipTypeWin32((byte) format.nativeId);
  697. if (format == PictureType.EMF) {
  698. bse.setBlipTypeMacOS((byte) PictureType.PICT.nativeId);
  699. } else if (format == PictureType.WMF) {
  700. bse.setBlipTypeMacOS((byte) PictureType.PICT.nativeId);
  701. } else if (format == PictureType.PICT) {
  702. bse.setBlipTypeWin32((byte) PictureType.WMF.nativeId);
  703. }
  704. bse.setRef(0);
  705. bse.setOffset(offset);
  706. bse.setRemainingData(new byte[0]);
  707. bstore.addChildRecord(bse);
  708. int count = bstore.getChildRecords().size();
  709. bstore.setOptions((short) ((count << 4) | 0xF));
  710. return pict;
  711. }
  712. /**
  713. * Adds a picture to this presentation and returns the associated index.
  714. *
  715. * @param pict
  716. * the file containing the image to add
  717. * @param format
  718. * the format of the picture. One of constans defined in the
  719. * <code>Picture</code> class.
  720. * @return the index to this picture (1 based).
  721. */
  722. public HSLFPictureData addPicture(File pict, PictureType format) throws IOException {
  723. int length = (int) pict.length();
  724. byte[] data = new byte[length];
  725. FileInputStream is = null;
  726. try {
  727. is = new FileInputStream(pict);
  728. is.read(data);
  729. } finally {
  730. if(is != null) is.close();
  731. }
  732. return addPicture(data, format);
  733. }
  734. /**
  735. * Add a font in this presentation
  736. *
  737. * @param font
  738. * the font to add
  739. * @return 0-based index of the font
  740. */
  741. public int addFont(PPFont font) {
  742. FontCollection fonts = getDocumentRecord().getEnvironment().getFontCollection();
  743. int idx = fonts.getFontIndex(font.getFontName());
  744. if (idx == -1) {
  745. idx = fonts.addFont(font.getFontName(), font.getCharSet(), font.getFontFlags(), font
  746. .getFontType(), font.getPitchAndFamily());
  747. }
  748. return idx;
  749. }
  750. /**
  751. * Get a font by index
  752. *
  753. * @param idx
  754. * 0-based index of the font
  755. * @return of an instance of <code>PPFont</code> or <code>null</code> if not
  756. * found
  757. */
  758. public PPFont getFont(int idx) {
  759. FontCollection fonts = getDocumentRecord().getEnvironment().getFontCollection();
  760. for (Record ch : fonts.getChildRecords()) {
  761. if (ch instanceof FontEntityAtom) {
  762. FontEntityAtom atom = (FontEntityAtom) ch;
  763. if (atom.getFontIndex() == idx) {
  764. return new PPFont(atom);
  765. }
  766. }
  767. }
  768. return null;
  769. }
  770. /**
  771. * get the number of fonts in the presentation
  772. *
  773. * @return number of fonts
  774. */
  775. public int getNumberOfFonts() {
  776. return getDocumentRecord().getEnvironment().getFontCollection().getNumberOfFonts();
  777. }
  778. /**
  779. * Return Header / Footer settings for slides
  780. *
  781. * @return Header / Footer settings for slides
  782. */
  783. public HeadersFooters getSlideHeadersFooters() {
  784. return new HeadersFooters(this, HeadersFootersContainer.SlideHeadersFootersContainer);
  785. }
  786. /**
  787. * Return Header / Footer settings for notes
  788. *
  789. * @return Header / Footer settings for notes
  790. */
  791. public HeadersFooters getNotesHeadersFooters() {
  792. if (_notes.isEmpty()) {
  793. return new HeadersFooters(this, HeadersFootersContainer.NotesHeadersFootersContainer);
  794. } else {
  795. return new HeadersFooters(_notes.get(0), HeadersFootersContainer.NotesHeadersFootersContainer);
  796. }
  797. }
  798. /**
  799. * Add a movie in this presentation
  800. *
  801. * @param path
  802. * the path or url to the movie
  803. * @return 0-based index of the movie
  804. */
  805. public int addMovie(String path, int type) {
  806. ExMCIMovie mci;
  807. switch (type) {
  808. case MovieShape.MOVIE_MPEG:
  809. mci = new ExMCIMovie();
  810. break;
  811. case MovieShape.MOVIE_AVI:
  812. mci = new ExAviMovie();
  813. break;
  814. default:
  815. throw new IllegalArgumentException("Unsupported Movie: " + type);
  816. }
  817. ExVideoContainer exVideo = mci.getExVideo();
  818. exVideo.getExMediaAtom().setMask(0xE80000);
  819. exVideo.getPathAtom().setText(path);
  820. int objectId = addToObjListAtom(mci);
  821. exVideo.getExMediaAtom().setObjectId(objectId);
  822. return objectId;
  823. }
  824. /**
  825. * Add a control in this presentation
  826. *
  827. * @param name
  828. * name of the control, e.g. "Shockwave Flash Object"
  829. * @param progId
  830. * OLE Programmatic Identifier, e.g.
  831. * "ShockwaveFlash.ShockwaveFlash.9"
  832. * @return 0-based index of the control
  833. */
  834. public int addControl(String name, String progId) {
  835. ExControl ctrl = new ExControl();
  836. ctrl.setProgId(progId);
  837. ctrl.setMenuName(name);
  838. ctrl.setClipboardName(name);
  839. ExOleObjAtom oleObj = ctrl.getExOleObjAtom();
  840. oleObj.setDrawAspect(ExOleObjAtom.DRAW_ASPECT_VISIBLE);
  841. oleObj.setType(ExOleObjAtom.TYPE_CONTROL);
  842. oleObj.setSubType(ExOleObjAtom.SUBTYPE_DEFAULT);
  843. int objectId = addToObjListAtom(ctrl);
  844. oleObj.setObjID(objectId);
  845. return objectId;
  846. }
  847. /**
  848. * Add a embedded object to this presentation
  849. *
  850. * @return 0-based index of the embedded object
  851. */
  852. public int addEmbed(POIFSFileSystem poiData) {
  853. DirectoryNode root = poiData.getRoot();
  854. // prepare embedded data
  855. if (new ClassID().equals(root.getStorageClsid())) {
  856. // need to set class id
  857. Map<String,ClassID> olemap = getOleMap();
  858. ClassID classID = null;
  859. for (Map.Entry<String,ClassID> entry : olemap.entrySet()) {
  860. if (root.hasEntry(entry.getKey())) {
  861. classID = entry.getValue();
  862. break;
  863. }
  864. }
  865. if (classID == null) {
  866. throw new IllegalArgumentException("Unsupported embedded document");
  867. }
  868. root.setStorageClsid(classID);
  869. }
  870. ExEmbed exEmbed = new ExEmbed();
  871. // remove unneccessary infos, so we don't need to specify the type
  872. // of the ole object multiple times
  873. Record children[] = exEmbed.getChildRecords();
  874. exEmbed.removeChild(children[2]);
  875. exEmbed.removeChild(children[3]);
  876. exEmbed.removeChild(children[4]);
  877. ExEmbedAtom eeEmbed = exEmbed.getExEmbedAtom();
  878. eeEmbed.setCantLockServerB(true);
  879. ExOleObjAtom eeAtom = exEmbed.getExOleObjAtom();
  880. eeAtom.setDrawAspect(ExOleObjAtom.DRAW_ASPECT_VISIBLE);
  881. eeAtom.setType(ExOleObjAtom.TYPE_EMBEDDED);
  882. // eeAtom.setSubType(ExOleObjAtom.SUBTYPE_EXCEL);
  883. // should be ignored?!?, see MS-PPT ExOleObjAtom, but Libre Office sets it ...
  884. eeAtom.setOptions(1226240);
  885. ExOleObjStg exOleObjStg = new ExOleObjStg();
  886. try {
  887. final String OLESTREAM_NAME = "\u0001Ole";
  888. if (!root.hasEntry(OLESTREAM_NAME)) {
  889. // the following data was taken from an example libre office document
  890. // beside this "\u0001Ole" record there were several other records, e.g. CompObj,
  891. // OlePresXXX, but it seems, that they aren't neccessary
  892. byte oleBytes[] = { 1, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
  893. poiData.createDocument(new ByteArrayInputStream(oleBytes), OLESTREAM_NAME);
  894. }
  895. ByteArrayOutputStream bos = new ByteArrayOutputStream();
  896. poiData.writeFilesystem(bos);
  897. exOleObjStg.setData(bos.toByteArray());
  898. } catch (IOException e) {
  899. throw new HSLFException(e);
  900. }
  901. int psrId = addPersistentObject(exOleObjStg);
  902. exOleObjStg.setPersistId(psrId);
  903. eeAtom.setObjStgDataRef(psrId);
  904. int objectId = addToObjListAtom(exEmbed);
  905. eeAtom.setObjID(objectId);
  906. return objectId;
  907. }
  908. protected int addToObjListAtom(RecordContainer exObj) {
  909. ExObjList lst = getDocumentRecord().getExObjList(true);
  910. ExObjListAtom objAtom = lst.getExObjListAtom();
  911. // increment the object ID seed
  912. int objectId = (int) objAtom.getObjectIDSeed() + 1;
  913. objAtom.setObjectIDSeed(objectId);
  914. lst.addChildAfter(exObj, objAtom);
  915. return objectId;
  916. }
  917. protected static Map<String,ClassID> getOleMap() {
  918. Map<String,ClassID> olemap = new HashMap<String,ClassID>();
  919. olemap.put("PowerPoint Document", ClassID.PPT_SHOW);
  920. olemap.put("Workbook", ClassID.EXCEL97); // as per BIFF8 spec
  921. olemap.put("WORKBOOK", ClassID.EXCEL97); // Typically from third party programs
  922. olemap.put("BOOK", ClassID.EXCEL97); // Typically odd Crystal Reports exports
  923. // ... to be continued
  924. return olemap;
  925. }
  926. protected int addPersistentObject(PositionDependentRecord slideRecord) {
  927. slideRecord.setLastOnDiskOffset(HSLFSlideShowImpl.UNSET_OFFSET);
  928. _hslfSlideShow.appendRootLevelRecord((Record)slideRecord);
  929. // For position dependent records, hold where they were and now are
  930. // As we go along, update, and hand over, to any Position Dependent
  931. // records we happen across
  932. Map<RecordTypes,PositionDependentRecord> interestingRecords =
  933. new HashMap<RecordTypes,PositionDependentRecord>();
  934. try {
  935. _hslfSlideShow.updateAndWriteDependantRecords(null,interestingRecords);
  936. } catch (IOException e) {
  937. throw new HSLFException(e);
  938. }
  939. PersistPtrHolder ptr = (PersistPtrHolder)interestingRecords.get(RecordTypes.PersistPtrIncrementalBlock);
  940. UserEditAtom usr = (UserEditAtom)interestingRecords.get(RecordTypes.UserEditAtom);
  941. // persist ID is UserEditAtom.maxPersistWritten + 1
  942. int psrId = usr.getMaxPersistWritten() + 1;
  943. // Last view is now of the slide
  944. usr.setLastViewType((short) UserEditAtom.LAST_VIEW_SLIDE_VIEW);
  945. // increment the number of persistent objects
  946. usr.setMaxPersistWritten(psrId);
  947. // Add the new slide into the last PersistPtr
  948. // (Also need to tell it where it is)
  949. int slideOffset = slideRecord.getLastOnDiskOffset();
  950. slideRecord.setLastOnDiskOffset(slideOffset);
  951. ptr.addSlideLookup(psrId, slideOffset);
  952. logger.log(POILogger.INFO, "New slide/object ended up at " + slideOffset);
  953. return psrId;
  954. }
  955. public MasterSheet<HSLFShape,HSLFTextParagraph> createMasterSheet() throws IOException {
  956. // TODO Auto-generated method stub
  957. return null;
  958. }
  959. public Resources getResources() {
  960. // TODO Auto-generated method stub
  961. return null;
  962. }
  963. @Override
  964. public void close() throws IOException {
  965. _hslfSlideShow.close();
  966. }
  967. }