| /* ==================================================================== |
| Licensed to the Apache Software Foundation (ASF) under one or more |
| contributor license agreements. See the NOTICE file distributed with |
| this work for additional information regarding copyright ownership. |
| The ASF licenses this file to You under the Apache License, Version 2.0 |
| (the "License"); you may not use this file except in compliance with |
| the License. You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| ==================================================================== */ |
| |
| package org.apache.poi.hslf.usermodel; |
| |
| import java.awt.Dimension; |
| import java.io.ByteArrayInputStream; |
| import java.io.ByteArrayOutputStream; |
| import java.io.Closeable; |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| |
| import org.apache.poi.ddf.EscherBSERecord; |
| import org.apache.poi.ddf.EscherContainerRecord; |
| import org.apache.poi.ddf.EscherOptRecord; |
| import org.apache.poi.hpsf.ClassID; |
| import org.apache.poi.hslf.exceptions.CorruptPowerPointFileException; |
| import org.apache.poi.hslf.exceptions.HSLFException; |
| import org.apache.poi.hslf.model.HeadersFooters; |
| import org.apache.poi.hslf.model.MovieShape; |
| import org.apache.poi.hslf.model.PPFont; |
| import org.apache.poi.hslf.record.*; |
| import org.apache.poi.hslf.record.SlideListWithText.SlideAtomsSet; |
| import org.apache.poi.poifs.filesystem.DirectoryNode; |
| import org.apache.poi.poifs.filesystem.NPOIFSFileSystem; |
| import org.apache.poi.poifs.filesystem.POIFSFileSystem; |
| import org.apache.poi.sl.usermodel.MasterSheet; |
| import org.apache.poi.sl.usermodel.PictureData.PictureType; |
| import org.apache.poi.sl.usermodel.Resources; |
| import org.apache.poi.sl.usermodel.SlideShow; |
| import org.apache.poi.util.IOUtils; |
| import org.apache.poi.util.POILogFactory; |
| import org.apache.poi.util.POILogger; |
| import org.apache.poi.util.Units; |
| |
| /** |
| * This class is a friendly wrapper on top of the more scary HSLFSlideShow. |
| * |
| * TODO: - figure out how to match notes to their correct sheet (will involve |
| * understanding DocSlideList and DocNotesList) - handle Slide creation cleaner |
| * |
| * @author Nick Burch |
| * @author Yegor kozlov |
| */ |
| public final class HSLFSlideShow implements SlideShow<HSLFShape,HSLFTextParagraph>, Closeable { |
| enum LoadSavePhase { |
| INIT, LOADED |
| } |
| private static final ThreadLocal<LoadSavePhase> loadSavePhase = new ThreadLocal<LoadSavePhase>(); |
| |
| // What we're based on |
| private final HSLFSlideShowImpl _hslfSlideShow; |
| |
| // Pointers to the most recent versions of the core records |
| // (Document, Notes, Slide etc) |
| private Record[] _mostRecentCoreRecords; |
| // Lookup between the PersitPtr "sheet" IDs, and the position |
| // in the mostRecentCoreRecords array |
| private Map<Integer,Integer> _sheetIdToCoreRecordsLookup; |
| |
| // Records that are interesting |
| private Document _documentRecord; |
| |
| // Friendly objects for people to deal with |
| private final List<HSLFSlideMaster> _masters = new ArrayList<HSLFSlideMaster>(); |
| private final List<HSLFTitleMaster> _titleMasters = new ArrayList<HSLFTitleMaster>(); |
| private final List<HSLFSlide> _slides = new ArrayList<HSLFSlide>(); |
| private final List<HSLFNotes> _notes = new ArrayList<HSLFNotes>(); |
| private FontCollection _fonts; |
| |
| // For logging |
| private static final POILogger logger = POILogFactory.getLogger(HSLFSlideShow.class); |
| |
| |
| /** |
| * Constructs a Powerpoint document from the underlying |
| * HSLFSlideShow object. Finds the model stuff from this |
| * |
| * @param hslfSlideShow the HSLFSlideShow to base on |
| */ |
| public HSLFSlideShow(HSLFSlideShowImpl hslfSlideShow) { |
| loadSavePhase.set(LoadSavePhase.INIT); |
| |
| // Get useful things from our base slideshow |
| _hslfSlideShow = hslfSlideShow; |
| |
| // Handle Parent-aware Records |
| for (Record record : _hslfSlideShow.getRecords()) { |
| if(record instanceof RecordContainer){ |
| RecordContainer.handleParentAwareRecords((RecordContainer)record); |
| } |
| } |
| |
| // Find the versions of the core records we'll want to use |
| findMostRecentCoreRecords(); |
| |
| // Build up the model level Slides and Notes |
| buildSlidesAndNotes(); |
| |
| loadSavePhase.set(LoadSavePhase.LOADED); |
| } |
| |
| /** |
| * Constructs a new, empty, Powerpoint document. |
| */ |
| public HSLFSlideShow() { |
| this(HSLFSlideShowImpl.create()); |
| } |
| |
| /** |
| * Constructs a Powerpoint document from an input stream. |
| */ |
| @SuppressWarnings("resource") |
| public HSLFSlideShow(InputStream inputStream) throws IOException { |
| this(new HSLFSlideShowImpl(inputStream)); |
| } |
| |
| /** |
| * Constructs a Powerpoint document from an POIFSFileSystem. |
| */ |
| @SuppressWarnings("resource") |
| public HSLFSlideShow(NPOIFSFileSystem npoifs) throws IOException { |
| this(new HSLFSlideShowImpl(npoifs)); |
| } |
| |
| /** |
| * Constructs a Powerpoint document from an DirectoryNode. |
| */ |
| @SuppressWarnings("resource") |
| public HSLFSlideShow(DirectoryNode root) throws IOException { |
| this(new HSLFSlideShowImpl(root)); |
| } |
| |
| /** |
| * @return the current loading/saving phase |
| */ |
| protected static LoadSavePhase getLoadSavePhase() { |
| return loadSavePhase.get(); |
| } |
| |
| /** |
| * Use the PersistPtrHolder entries to figure out what is the "most recent" |
| * version of all the core records (Document, Notes, Slide etc), and save a |
| * record of them. Do this by walking from the oldest PersistPtr to the |
| * newest, overwriting any references found along the way with newer ones |
| */ |
| private void findMostRecentCoreRecords() { |
| // To start with, find the most recent in the byte offset domain |
| Map<Integer,Integer> mostRecentByBytes = new HashMap<Integer,Integer>(); |
| for (Record record : _hslfSlideShow.getRecords()) { |
| if (record instanceof PersistPtrHolder) { |
| PersistPtrHolder pph = (PersistPtrHolder) record; |
| |
| // If we've already seen any of the "slide" IDs for this |
| // PersistPtr, remove their old positions |
| int[] ids = pph.getKnownSlideIDs(); |
| for (int id : ids) { |
| if (mostRecentByBytes.containsKey(id)) { |
| mostRecentByBytes.remove(id); |
| } |
| } |
| |
| // Now, update the byte level locations with their latest values |
| Map<Integer,Integer> thisSetOfLocations = pph.getSlideLocationsLookup(); |
| for (int id : ids) { |
| mostRecentByBytes.put(id, thisSetOfLocations.get(id)); |
| } |
| } |
| } |
| |
| // We now know how many unique special records we have, so init |
| // the array |
| _mostRecentCoreRecords = new Record[mostRecentByBytes.size()]; |
| |
| // We'll also want to be able to turn the slide IDs into a position |
| // in this array |
| _sheetIdToCoreRecordsLookup = new HashMap<Integer,Integer>(); |
| Integer[] allIDs = mostRecentByBytes.keySet().toArray(new Integer[mostRecentByBytes.size()]); |
| Arrays.sort(allIDs); |
| for (int i = 0; i < allIDs.length; i++) { |
| _sheetIdToCoreRecordsLookup.put(allIDs[i], i); |
| } |
| |
| Map<Integer,Integer> mostRecentByBytesRev = new HashMap<Integer,Integer>(mostRecentByBytes.size()); |
| for (Map.Entry<Integer,Integer> me : mostRecentByBytes.entrySet()) { |
| mostRecentByBytesRev.put(me.getValue(), me.getKey()); |
| } |
| |
| // Now convert the byte offsets back into record offsets |
| for (Record record : _hslfSlideShow.getRecords()) { |
| if (!(record instanceof PositionDependentRecord)) continue; |
| |
| PositionDependentRecord pdr = (PositionDependentRecord) record; |
| int recordAt = pdr.getLastOnDiskOffset(); |
| |
| Integer thisID = mostRecentByBytesRev.get(recordAt); |
| |
| if (thisID == null) continue; |
| |
| // Bingo. Now, where do we store it? |
| int storeAt = _sheetIdToCoreRecordsLookup.get(thisID); |
| |
| // Tell it its Sheet ID, if it cares |
| if (pdr instanceof PositionDependentRecordContainer) { |
| PositionDependentRecordContainer pdrc = (PositionDependentRecordContainer) record; |
| pdrc.setSheetId(thisID); |
| } |
| |
| // Finally, save the record |
| _mostRecentCoreRecords[storeAt] = record; |
| } |
| |
| // Now look for the interesting records in there |
| for (Record record : _mostRecentCoreRecords) { |
| // Check there really is a record at this number |
| if (record != null) { |
| // Find the Document, and interesting things in it |
| if (record.getRecordType() == RecordTypes.Document.typeID) { |
| _documentRecord = (Document) record; |
| _fonts = _documentRecord.getEnvironment().getFontCollection(); |
| } |
| } /*else { |
| // No record at this number |
| // Odd, but not normally a problem |
| }*/ |
| } |
| } |
| |
| /** |
| * For a given SlideAtomsSet, return the core record, based on the refID |
| * from the SlidePersistAtom |
| */ |
| private Record getCoreRecordForSAS(SlideAtomsSet sas) { |
| SlidePersistAtom spa = sas.getSlidePersistAtom(); |
| int refID = spa.getRefID(); |
| return getCoreRecordForRefID(refID); |
| } |
| |
| /** |
| * For a given refID (the internal, 0 based numbering scheme), return the |
| * core record |
| * |
| * @param refID |
| * the refID |
| */ |
| private Record getCoreRecordForRefID(int refID) { |
| Integer coreRecordId = _sheetIdToCoreRecordsLookup.get(refID); |
| if (coreRecordId != null) { |
| return _mostRecentCoreRecords[coreRecordId]; |
| } |
| logger.log(POILogger.ERROR, |
| "We tried to look up a reference to a core record, but there was no core ID for reference ID " |
| + refID); |
| return null; |
| } |
| |
| /** |
| * Build up model level Slide and Notes objects, from the underlying |
| * records. |
| */ |
| private void buildSlidesAndNotes() { |
| // Ensure we really found a Document record earlier |
| // If we didn't, then the file is probably corrupt |
| if (_documentRecord == null) { |
| throw new CorruptPowerPointFileException( |
| "The PowerPoint file didn't contain a Document Record in its PersistPtr blocks. It is probably corrupt."); |
| } |
| |
| // Fetch the SlideListWithTexts in the most up-to-date Document Record |
| // |
| // As far as we understand it: |
| // * The first SlideListWithText will contain a SlideAtomsSet |
| // for each of the master slides |
| // * The second SlideListWithText will contain a SlideAtomsSet |
| // for each of the slides, in their current order |
| // These SlideAtomsSets will normally contain text |
| // * The third SlideListWithText (if present), will contain a |
| // SlideAtomsSet for each Notes |
| // These SlideAtomsSets will not normally contain text |
| // |
| // Having indentified the masters, slides and notes + their orders, |
| // we have to go and find their matching records |
| // We always use the latest versions of these records, and use the |
| // SlideAtom/NotesAtom to match them with the StyleAtomSet |
| |
| findMasterSlides(); |
| |
| // Having sorted out the masters, that leaves the notes and slides |
| Map<Integer,Integer> slideIdToNotes = new HashMap<Integer,Integer>(); |
| |
| // Start by finding the notes records |
| findNotesSlides(slideIdToNotes); |
| |
| // Now, do the same thing for our slides |
| findSlides(slideIdToNotes); |
| } |
| |
| /** |
| * Find master slides |
| * These can be MainMaster records, but oddly they can also be |
| * Slides or Notes, and possibly even other odd stuff.... |
| * About the only thing you can say is that the master details are in the first SLWT. |
| */ |
| private void findMasterSlides() { |
| SlideListWithText masterSLWT = _documentRecord.getMasterSlideListWithText(); |
| if (masterSLWT == null) { |
| return; |
| } |
| |
| for (SlideAtomsSet sas : masterSLWT.getSlideAtomsSets()) { |
| Record r = getCoreRecordForSAS(sas); |
| int sheetNo = sas.getSlidePersistAtom().getSlideIdentifier(); |
| if (r instanceof Slide) { |
| HSLFTitleMaster master = new HSLFTitleMaster((Slide)r, sheetNo); |
| master.setSlideShow(this); |
| _titleMasters.add(master); |
| } else if (r instanceof MainMaster) { |
| HSLFSlideMaster master = new HSLFSlideMaster((MainMaster)r, sheetNo); |
| master.setSlideShow(this); |
| _masters.add(master); |
| } |
| } |
| } |
| |
| private void findNotesSlides(Map<Integer,Integer> slideIdToNotes) { |
| SlideListWithText notesSLWT = _documentRecord.getNotesSlideListWithText(); |
| |
| if (notesSLWT == null) { |
| return; |
| } |
| |
| // Match up the records and the SlideAtomSets |
| int idx = -1; |
| for (SlideAtomsSet notesSet : notesSLWT.getSlideAtomsSets()) { |
| idx++; |
| // Get the right core record |
| Record r = getCoreRecordForSAS(notesSet); |
| SlidePersistAtom spa = notesSet.getSlidePersistAtom(); |
| |
| String loggerLoc = "A Notes SlideAtomSet at "+idx+" said its record was at refID "+spa.getRefID(); |
| |
| // we need to add null-records, otherwise the index references to other existing don't work anymore |
| if (r == null) { |
| logger.log(POILogger.WARN, loggerLoc+", but that record didn't exist - record ignored."); |
| continue; |
| } |
| |
| // Ensure it really is a notes record |
| if (!(r instanceof Notes)) { |
| logger.log(POILogger.ERROR, loggerLoc+", but that was actually a " + r); |
| continue; |
| } |
| |
| Notes notesRecord = (Notes) r; |
| |
| // Record the match between slide id and these notes |
| int slideId = spa.getSlideIdentifier(); |
| slideIdToNotes.put(slideId, idx); |
| |
| HSLFNotes hn = new HSLFNotes(notesRecord); |
| hn.setSlideShow(this); |
| _notes.add(hn); |
| } |
| } |
| |
| private void findSlides(Map<Integer,Integer> slideIdToNotes) { |
| SlideListWithText slidesSLWT = _documentRecord.getSlideSlideListWithText(); |
| if (slidesSLWT == null) { |
| return; |
| } |
| |
| // Match up the records and the SlideAtomSets |
| int idx = -1; |
| for (SlideAtomsSet sas : slidesSLWT.getSlideAtomsSets()) { |
| idx++; |
| // Get the right core record |
| SlidePersistAtom spa = sas.getSlidePersistAtom(); |
| Record r = getCoreRecordForSAS(sas); |
| |
| // Ensure it really is a slide record |
| if (!(r instanceof Slide)) { |
| logger.log(POILogger.ERROR, "A Slide SlideAtomSet at " + idx |
| + " said its record was at refID " |
| + spa.getRefID() |
| + ", but that was actually a " + r); |
| continue; |
| } |
| |
| Slide slide = (Slide)r; |
| |
| // Do we have a notes for this? |
| HSLFNotes notes = null; |
| // Slide.SlideAtom.notesId references the corresponding notes slide. |
| // 0 if slide has no notes. |
| int noteId = slide.getSlideAtom().getNotesID(); |
| if (noteId != 0) { |
| Integer notesPos = slideIdToNotes.get(noteId); |
| if (notesPos != null && 0 <= notesPos && notesPos < _notes.size()) { |
| notes = _notes.get(notesPos); |
| } else { |
| logger.log(POILogger.ERROR, "Notes not found for noteId=" + noteId); |
| } |
| } |
| |
| // Now, build our slide |
| int slideIdentifier = spa.getSlideIdentifier(); |
| HSLFSlide hs = new HSLFSlide(slide, notes, sas, slideIdentifier, (idx + 1)); |
| hs.setSlideShow(this); |
| _slides.add(hs); |
| } |
| } |
| |
| @Override |
| public void write(OutputStream out) throws IOException { |
| // check for text paragraph modifications |
| for (HSLFSlide sl : getSlides()) { |
| writeDirtyParagraphs(sl); |
| } |
| |
| for (HSLFSlideMaster sl : getSlideMasters()) { |
| boolean isDirty = false; |
| for (List<HSLFTextParagraph> paras : sl.getTextParagraphs()) { |
| for (HSLFTextParagraph p : paras) { |
| isDirty |= p.isDirty(); |
| } |
| } |
| if (isDirty) { |
| for (TxMasterStyleAtom sa : sl.getTxMasterStyleAtoms()) { |
| if (sa != null) { |
| // not all master style atoms are set - index 3 is typically null |
| sa.updateStyles(); |
| } |
| } |
| } |
| } |
| |
| _hslfSlideShow.write(out); |
| } |
| |
| private void writeDirtyParagraphs(HSLFShapeContainer container) { |
| for (HSLFShape sh : container.getShapes()) { |
| if (sh instanceof HSLFShapeContainer) { |
| writeDirtyParagraphs((HSLFShapeContainer)sh); |
| } else if (sh instanceof HSLFTextShape) { |
| HSLFTextShape hts = (HSLFTextShape)sh; |
| boolean isDirty = false; |
| for (HSLFTextParagraph p : hts.getTextParagraphs()) { |
| isDirty |= p.isDirty(); |
| } |
| if (isDirty) hts.storeText(); |
| } |
| } |
| } |
| |
| /** |
| * Returns an array of the most recent version of all the interesting |
| * records |
| */ |
| public Record[] getMostRecentCoreRecords() { |
| return _mostRecentCoreRecords; |
| } |
| |
| /** |
| * Returns an array of all the normal Slides found in the slideshow |
| */ |
| @Override |
| public List<HSLFSlide> getSlides() { |
| return _slides; |
| } |
| |
| /** |
| * Returns an array of all the normal Notes found in the slideshow |
| */ |
| public List<HSLFNotes> getNotes() { |
| return _notes; |
| } |
| |
| /** |
| * Returns an array of all the normal Slide Masters found in the slideshow |
| */ |
| @Override |
| public List<HSLFSlideMaster> getSlideMasters() { |
| return _masters; |
| } |
| |
| /** |
| * Returns an array of all the normal Title Masters found in the slideshow |
| */ |
| public List<HSLFTitleMaster> getTitleMasters() { |
| return _titleMasters; |
| } |
| |
| @Override |
| public List<HSLFPictureData> getPictureData() { |
| return _hslfSlideShow.getPictureData(); |
| } |
| |
| /** |
| * Returns the data of all the embedded OLE object in the SlideShow |
| */ |
| public HSLFObjectData[] getEmbeddedObjects() { |
| return _hslfSlideShow.getEmbeddedObjects(); |
| } |
| |
| /** |
| * Returns the data of all the embedded sounds in the SlideShow |
| */ |
| public HSLFSoundData[] getSoundData() { |
| return HSLFSoundData.find(_documentRecord); |
| } |
| |
| @Override |
| public Dimension getPageSize() { |
| DocumentAtom docatom = _documentRecord.getDocumentAtom(); |
| int pgx = (int)Units.masterToPoints((int)docatom.getSlideSizeX()); |
| int pgy = (int)Units.masterToPoints((int)docatom.getSlideSizeY()); |
| return new Dimension(pgx, pgy); |
| } |
| |
| @Override |
| public void setPageSize(Dimension pgsize) { |
| DocumentAtom docatom = _documentRecord.getDocumentAtom(); |
| docatom.setSlideSizeX(Units.pointsToMaster(pgsize.width)); |
| docatom.setSlideSizeY(Units.pointsToMaster(pgsize.height)); |
| } |
| |
| /** |
| * Helper method for usermodel: Get the font collection |
| */ |
| protected FontCollection getFontCollection() { |
| return _fonts; |
| } |
| |
| /** |
| * Helper method for usermodel and model: Get the document record |
| */ |
| public Document getDocumentRecord() { |
| return _documentRecord; |
| } |
| |
| /** |
| * Re-orders a slide, to a new position. |
| * |
| * @param oldSlideNumber |
| * The old slide number (1 based) |
| * @param newSlideNumber |
| * The new slide number (1 based) |
| */ |
| public void reorderSlide(int oldSlideNumber, int newSlideNumber) { |
| // Ensure these numbers are valid |
| if (oldSlideNumber < 1 || newSlideNumber < 1) { |
| throw new IllegalArgumentException("Old and new slide numbers must be greater than 0"); |
| } |
| if (oldSlideNumber > _slides.size() || newSlideNumber > _slides.size()) { |
| throw new IllegalArgumentException( |
| "Old and new slide numbers must not exceed the number of slides (" |
| + _slides.size() + ")"); |
| } |
| |
| // The order of slides is defined by the order of slide atom sets in the |
| // SlideListWithText container. |
| SlideListWithText slwt = _documentRecord.getSlideSlideListWithText(); |
| SlideAtomsSet[] sas = slwt.getSlideAtomsSets(); |
| |
| SlideAtomsSet tmp = sas[oldSlideNumber - 1]; |
| sas[oldSlideNumber - 1] = sas[newSlideNumber - 1]; |
| sas[newSlideNumber - 1] = tmp; |
| |
| Collections.swap(_slides, oldSlideNumber - 1, newSlideNumber - 1); |
| _slides.get(newSlideNumber - 1).setSlideNumber(newSlideNumber); |
| _slides.get(oldSlideNumber - 1).setSlideNumber(oldSlideNumber); |
| |
| ArrayList<Record> lst = new ArrayList<Record>(); |
| for (SlideAtomsSet s : sas) { |
| lst.add(s.getSlidePersistAtom()); |
| lst.addAll(Arrays.asList(s.getSlideRecords())); |
| } |
| |
| Record[] r = lst.toArray(new Record[lst.size()]); |
| slwt.setChildRecord(r); |
| } |
| |
| /** |
| * Removes the slide at the given index (0-based). |
| * <p> |
| * Shifts any subsequent slides to the left (subtracts one from their slide |
| * numbers). |
| * </p> |
| * |
| * @param index |
| * the index of the slide to remove (0-based) |
| * @return the slide that was removed from the slide show. |
| */ |
| public HSLFSlide removeSlide(int index) { |
| int lastSlideIdx = _slides.size() - 1; |
| if (index < 0 || index > lastSlideIdx) { |
| throw new IllegalArgumentException("Slide index (" + index + ") is out of range (0.." |
| + lastSlideIdx + ")"); |
| } |
| |
| SlideListWithText slwt = _documentRecord.getSlideSlideListWithText(); |
| SlideAtomsSet[] sas = slwt.getSlideAtomsSets(); |
| |
| List<Record> records = new ArrayList<Record>(); |
| List<SlideAtomsSet> sa = new ArrayList<SlideAtomsSet>(Arrays.asList(sas)); |
| |
| HSLFSlide removedSlide = _slides.remove(index); |
| _notes.remove(removedSlide.getNotes()); |
| sa.remove(index); |
| |
| int i=0; |
| for (HSLFSlide s : _slides) s.setSlideNumber(i++); |
| |
| for (SlideAtomsSet s : sa) { |
| records.add(s.getSlidePersistAtom()); |
| records.addAll(Arrays.asList(s.getSlideRecords())); |
| } |
| if (sa.isEmpty()) { |
| _documentRecord.removeSlideListWithText(slwt); |
| } else { |
| slwt.setSlideAtomsSets(sa.toArray(new SlideAtomsSet[sa.size()])); |
| slwt.setChildRecord(records.toArray(new Record[records.size()])); |
| } |
| |
| // if the removed slide had notes - remove references to them too |
| |
| int notesId = removedSlide.getSlideRecord().getSlideAtom().getNotesID(); |
| if (notesId != 0) { |
| SlideListWithText nslwt = _documentRecord.getNotesSlideListWithText(); |
| records = new ArrayList<Record>(); |
| ArrayList<SlideAtomsSet> na = new ArrayList<SlideAtomsSet>(); |
| for (SlideAtomsSet ns : nslwt.getSlideAtomsSets()) { |
| if (ns.getSlidePersistAtom().getSlideIdentifier() == notesId) continue; |
| na.add(ns); |
| records.add(ns.getSlidePersistAtom()); |
| if (ns.getSlideRecords() != null) { |
| records.addAll(Arrays.asList(ns.getSlideRecords())); |
| } |
| } |
| if (na.isEmpty()) { |
| _documentRecord.removeSlideListWithText(nslwt); |
| } else { |
| nslwt.setSlideAtomsSets(na.toArray(new SlideAtomsSet[na.size()])); |
| nslwt.setChildRecord(records.toArray(new Record[records.size()])); |
| } |
| } |
| |
| return removedSlide; |
| } |
| |
| /** |
| * Create a blank <code>Slide</code>. |
| * |
| * @return the created <code>Slide</code> |
| */ |
| @Override |
| public HSLFSlide createSlide() { |
| // We need to add the records to the SLWT that deals |
| // with Slides. |
| // Add it, if it doesn't exist |
| SlideListWithText slist = _documentRecord.getSlideSlideListWithText(); |
| if (slist == null) { |
| // Need to add a new one |
| slist = new SlideListWithText(); |
| slist.setInstance(SlideListWithText.SLIDES); |
| _documentRecord.addSlideListWithText(slist); |
| } |
| |
| // Grab the SlidePersistAtom with the highest Slide Number. |
| // (Will stay as null if no SlidePersistAtom exists yet in |
| // the slide, or only master slide's ones do) |
| SlidePersistAtom prev = null; |
| for (SlideAtomsSet sas : slist.getSlideAtomsSets()) { |
| SlidePersistAtom spa = sas.getSlidePersistAtom(); |
| if (spa.getSlideIdentifier() < 0) { |
| // This is for a master slide |
| // Odd, since we only deal with the Slide SLWT |
| } else { |
| // Must be for a real slide |
| if (prev == null) { |
| prev = spa; |
| } |
| if (prev.getSlideIdentifier() < spa.getSlideIdentifier()) { |
| prev = spa; |
| } |
| } |
| } |
| |
| // Set up a new SlidePersistAtom for this slide |
| SlidePersistAtom sp = new SlidePersistAtom(); |
| |
| // First slideId is always 256 |
| sp.setSlideIdentifier(prev == null ? 256 : (prev.getSlideIdentifier() + 1)); |
| |
| // Add this new SlidePersistAtom to the SlideListWithText |
| slist.addSlidePersistAtom(sp); |
| |
| // Create a new Slide |
| HSLFSlide slide = new HSLFSlide(sp.getSlideIdentifier(), sp.getRefID(), _slides.size() + 1); |
| slide.setSlideShow(this); |
| slide.onCreate(); |
| |
| // Add in to the list of Slides |
| _slides.add(slide); |
| logger.log(POILogger.INFO, "Added slide " + _slides.size() + " with ref " + sp.getRefID() |
| + " and identifier " + sp.getSlideIdentifier()); |
| |
| // Add the core records for this new Slide to the record tree |
| Slide slideRecord = slide.getSlideRecord(); |
| int psrId = addPersistentObject(slideRecord); |
| sp.setRefID(psrId); |
| slideRecord.setSheetId(psrId); |
| |
| slide.setMasterSheet(_masters.get(0)); |
| // All done and added |
| return slide; |
| } |
| |
| @Override |
| public HSLFPictureData addPicture(byte[] data, PictureType format) throws IOException { |
| if (format == null || format.nativeId == -1) { |
| throw new IllegalArgumentException("Unsupported picture format: " + format); |
| } |
| |
| HSLFPictureData pd = findPictureData(data); |
| if (pd != null) { |
| // identical picture was already added to the SlideShow |
| return pd; |
| } |
| |
| EscherContainerRecord bstore; |
| |
| EscherContainerRecord dggContainer = _documentRecord.getPPDrawingGroup().getDggContainer(); |
| bstore = (EscherContainerRecord) HSLFShape.getEscherChild(dggContainer, |
| EscherContainerRecord.BSTORE_CONTAINER); |
| if (bstore == null) { |
| bstore = new EscherContainerRecord(); |
| bstore.setRecordId(EscherContainerRecord.BSTORE_CONTAINER); |
| |
| dggContainer.addChildBefore(bstore, EscherOptRecord.RECORD_ID); |
| } |
| |
| HSLFPictureData pict = HSLFPictureData.create(format); |
| pict.setData(data); |
| |
| int offset = _hslfSlideShow.addPicture(pict); |
| |
| EscherBSERecord bse = new EscherBSERecord(); |
| bse.setRecordId(EscherBSERecord.RECORD_ID); |
| bse.setOptions((short) (0x0002 | (format.nativeId << 4))); |
| bse.setSize(pict.getRawData().length + 8); |
| byte[] uid = HSLFPictureData.getChecksum(data); |
| bse.setUid(uid); |
| |
| bse.setBlipTypeMacOS((byte) format.nativeId); |
| bse.setBlipTypeWin32((byte) format.nativeId); |
| |
| if (format == PictureType.EMF) { |
| bse.setBlipTypeMacOS((byte) PictureType.PICT.nativeId); |
| } else if (format == PictureType.WMF) { |
| bse.setBlipTypeMacOS((byte) PictureType.PICT.nativeId); |
| } else if (format == PictureType.PICT) { |
| bse.setBlipTypeWin32((byte) PictureType.WMF.nativeId); |
| } |
| |
| bse.setRef(0); |
| bse.setOffset(offset); |
| bse.setRemainingData(new byte[0]); |
| |
| bstore.addChildRecord(bse); |
| int count = bstore.getChildRecords().size(); |
| bstore.setOptions((short) ((count << 4) | 0xF)); |
| |
| return pict; |
| } |
| |
| /** |
| * Adds a picture to the presentation. |
| * |
| * @param is The stream to read the image from |
| * @param format The format of the picture. |
| * |
| * @return the picture data. |
| * @since 3.15 beta 2 |
| */ |
| @Override |
| public HSLFPictureData addPicture(InputStream is, PictureType format) throws IOException { |
| if (format == null || format.nativeId == -1) { // fail early |
| throw new IllegalArgumentException("Unsupported picture format: " + format); |
| } |
| return addPicture(IOUtils.toByteArray(is), format); |
| } |
| |
| /** |
| * Adds a picture to the presentation. |
| * |
| * @param pict |
| * the file containing the image to add |
| * @param format |
| * The format of the picture. |
| * |
| * @return the picture data. |
| * @since 3.15 beta 2 |
| */ |
| @Override |
| public HSLFPictureData addPicture(File pict, PictureType format) throws IOException { |
| if (format == null || format.nativeId == -1) { // fail early |
| throw new IllegalArgumentException("Unsupported picture format: " + format); |
| } |
| int length = (int) pict.length(); |
| byte[] data = new byte[length]; |
| FileInputStream is = new FileInputStream(pict); |
| try { |
| IOUtils.readFully(is, data); |
| } finally { |
| is.close(); |
| } |
| return addPicture(data, format); |
| } |
| |
| /** |
| * check if a picture with this picture data already exists in this presentation |
| * |
| * @param pictureData The picture data to find in the SlideShow |
| * @return {@code null} if picture data is not found in this slideshow |
| * @since 3.15 beta 3 |
| */ |
| @Override |
| public HSLFPictureData findPictureData(byte[] pictureData) { |
| byte[] uid = HSLFPictureData.getChecksum(pictureData); |
| |
| for (HSLFPictureData pic : getPictureData()) { |
| if (Arrays.equals(pic.getUID(), uid)) { |
| return pic; |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Add a font in this presentation |
| * |
| * @param font |
| * the font to add |
| * @return 0-based index of the font |
| */ |
| public int addFont(PPFont font) { |
| FontCollection fonts = getDocumentRecord().getEnvironment().getFontCollection(); |
| int idx = fonts.getFontIndex(font.getFontName()); |
| if (idx == -1) { |
| idx = fonts.addFont(font.getFontName(), font.getCharSet(), font.getFontFlags(), font |
| .getFontType(), font.getPitchAndFamily()); |
| } |
| return idx; |
| } |
| |
| /** |
| * Get a font by index |
| * |
| * @param idx |
| * 0-based index of the font |
| * @return of an instance of <code>PPFont</code> or <code>null</code> if not |
| * found |
| */ |
| public PPFont getFont(int idx) { |
| FontCollection fonts = getDocumentRecord().getEnvironment().getFontCollection(); |
| for (Record ch : fonts.getChildRecords()) { |
| if (ch instanceof FontEntityAtom) { |
| FontEntityAtom atom = (FontEntityAtom) ch; |
| if (atom.getFontIndex() == idx) { |
| return new PPFont(atom); |
| } |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * get the number of fonts in the presentation |
| * |
| * @return number of fonts |
| */ |
| public int getNumberOfFonts() { |
| return getDocumentRecord().getEnvironment().getFontCollection().getNumberOfFonts(); |
| } |
| |
| /** |
| * Return Header / Footer settings for slides |
| * |
| * @return Header / Footer settings for slides |
| */ |
| public HeadersFooters getSlideHeadersFooters() { |
| return new HeadersFooters(this, HeadersFootersContainer.SlideHeadersFootersContainer); |
| } |
| |
| /** |
| * Return Header / Footer settings for notes |
| * |
| * @return Header / Footer settings for notes |
| */ |
| public HeadersFooters getNotesHeadersFooters() { |
| if (_notes.isEmpty()) { |
| return new HeadersFooters(this, HeadersFootersContainer.NotesHeadersFootersContainer); |
| } else { |
| return new HeadersFooters(_notes.get(0), HeadersFootersContainer.NotesHeadersFootersContainer); |
| } |
| } |
| |
| /** |
| * Add a movie in this presentation |
| * |
| * @param path |
| * the path or url to the movie |
| * @return 0-based index of the movie |
| */ |
| public int addMovie(String path, int type) { |
| ExMCIMovie mci; |
| switch (type) { |
| case MovieShape.MOVIE_MPEG: |
| mci = new ExMCIMovie(); |
| break; |
| case MovieShape.MOVIE_AVI: |
| mci = new ExAviMovie(); |
| break; |
| default: |
| throw new IllegalArgumentException("Unsupported Movie: " + type); |
| } |
| |
| ExVideoContainer exVideo = mci.getExVideo(); |
| exVideo.getExMediaAtom().setMask(0xE80000); |
| exVideo.getPathAtom().setText(path); |
| |
| int objectId = addToObjListAtom(mci); |
| exVideo.getExMediaAtom().setObjectId(objectId); |
| |
| return objectId; |
| } |
| |
| /** |
| * Add a control in this presentation |
| * |
| * @param name |
| * name of the control, e.g. "Shockwave Flash Object" |
| * @param progId |
| * OLE Programmatic Identifier, e.g. |
| * "ShockwaveFlash.ShockwaveFlash.9" |
| * @return 0-based index of the control |
| */ |
| public int addControl(String name, String progId) { |
| ExControl ctrl = new ExControl(); |
| ctrl.setProgId(progId); |
| ctrl.setMenuName(name); |
| ctrl.setClipboardName(name); |
| |
| ExOleObjAtom oleObj = ctrl.getExOleObjAtom(); |
| oleObj.setDrawAspect(ExOleObjAtom.DRAW_ASPECT_VISIBLE); |
| oleObj.setType(ExOleObjAtom.TYPE_CONTROL); |
| oleObj.setSubType(ExOleObjAtom.SUBTYPE_DEFAULT); |
| |
| int objectId = addToObjListAtom(ctrl); |
| oleObj.setObjID(objectId); |
| return objectId; |
| } |
| |
| /** |
| * Add a embedded object to this presentation |
| * |
| * @return 0-based index of the embedded object |
| */ |
| public int addEmbed(POIFSFileSystem poiData) { |
| DirectoryNode root = poiData.getRoot(); |
| |
| // prepare embedded data |
| if (new ClassID().equals(root.getStorageClsid())) { |
| // need to set class id |
| Map<String,ClassID> olemap = getOleMap(); |
| ClassID classID = null; |
| for (Map.Entry<String,ClassID> entry : olemap.entrySet()) { |
| if (root.hasEntry(entry.getKey())) { |
| classID = entry.getValue(); |
| break; |
| } |
| } |
| if (classID == null) { |
| throw new IllegalArgumentException("Unsupported embedded document"); |
| } |
| |
| root.setStorageClsid(classID); |
| } |
| |
| ExEmbed exEmbed = new ExEmbed(); |
| // remove unneccessary infos, so we don't need to specify the type |
| // of the ole object multiple times |
| Record children[] = exEmbed.getChildRecords(); |
| exEmbed.removeChild(children[2]); |
| exEmbed.removeChild(children[3]); |
| exEmbed.removeChild(children[4]); |
| |
| ExEmbedAtom eeEmbed = exEmbed.getExEmbedAtom(); |
| eeEmbed.setCantLockServerB(true); |
| |
| ExOleObjAtom eeAtom = exEmbed.getExOleObjAtom(); |
| eeAtom.setDrawAspect(ExOleObjAtom.DRAW_ASPECT_VISIBLE); |
| eeAtom.setType(ExOleObjAtom.TYPE_EMBEDDED); |
| // eeAtom.setSubType(ExOleObjAtom.SUBTYPE_EXCEL); |
| // should be ignored?!?, see MS-PPT ExOleObjAtom, but Libre Office sets it ... |
| eeAtom.setOptions(1226240); |
| |
| ExOleObjStg exOleObjStg = new ExOleObjStg(); |
| try { |
| final String OLESTREAM_NAME = "\u0001Ole"; |
| if (!root.hasEntry(OLESTREAM_NAME)) { |
| // the following data was taken from an example libre office document |
| // beside this "\u0001Ole" record there were several other records, e.g. CompObj, |
| // OlePresXXX, but it seems, that they aren't neccessary |
| byte oleBytes[] = { 1, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; |
| poiData.createDocument(new ByteArrayInputStream(oleBytes), OLESTREAM_NAME); |
| } |
| |
| ByteArrayOutputStream bos = new ByteArrayOutputStream(); |
| poiData.writeFilesystem(bos); |
| exOleObjStg.setData(bos.toByteArray()); |
| } catch (IOException e) { |
| throw new HSLFException(e); |
| } |
| |
| int psrId = addPersistentObject(exOleObjStg); |
| exOleObjStg.setPersistId(psrId); |
| eeAtom.setObjStgDataRef(psrId); |
| |
| int objectId = addToObjListAtom(exEmbed); |
| eeAtom.setObjID(objectId); |
| return objectId; |
| } |
| |
| protected int addToObjListAtom(RecordContainer exObj) { |
| ExObjList lst = getDocumentRecord().getExObjList(true); |
| ExObjListAtom objAtom = lst.getExObjListAtom(); |
| // increment the object ID seed |
| int objectId = (int) objAtom.getObjectIDSeed() + 1; |
| objAtom.setObjectIDSeed(objectId); |
| |
| lst.addChildAfter(exObj, objAtom); |
| |
| return objectId; |
| } |
| |
| protected static Map<String,ClassID> getOleMap() { |
| Map<String,ClassID> olemap = new HashMap<String,ClassID>(); |
| olemap.put("PowerPoint Document", ClassID.PPT_SHOW); |
| olemap.put("Workbook", ClassID.EXCEL97); // as per BIFF8 spec |
| olemap.put("WORKBOOK", ClassID.EXCEL97); // Typically from third party programs |
| olemap.put("BOOK", ClassID.EXCEL97); // Typically odd Crystal Reports exports |
| // ... to be continued |
| return olemap; |
| } |
| |
| protected int addPersistentObject(PositionDependentRecord slideRecord) { |
| slideRecord.setLastOnDiskOffset(HSLFSlideShowImpl.UNSET_OFFSET); |
| _hslfSlideShow.appendRootLevelRecord((Record)slideRecord); |
| |
| // For position dependent records, hold where they were and now are |
| // As we go along, update, and hand over, to any Position Dependent |
| // records we happen across |
| Map<RecordTypes,PositionDependentRecord> interestingRecords = |
| new HashMap<RecordTypes,PositionDependentRecord>(); |
| |
| try { |
| _hslfSlideShow.updateAndWriteDependantRecords(null,interestingRecords); |
| } catch (IOException e) { |
| throw new HSLFException(e); |
| } |
| |
| PersistPtrHolder ptr = (PersistPtrHolder)interestingRecords.get(RecordTypes.PersistPtrIncrementalBlock); |
| UserEditAtom usr = (UserEditAtom)interestingRecords.get(RecordTypes.UserEditAtom); |
| |
| // persist ID is UserEditAtom.maxPersistWritten + 1 |
| int psrId = usr.getMaxPersistWritten() + 1; |
| |
| // Last view is now of the slide |
| usr.setLastViewType((short) UserEditAtom.LAST_VIEW_SLIDE_VIEW); |
| // increment the number of persistent objects |
| usr.setMaxPersistWritten(psrId); |
| |
| // Add the new slide into the last PersistPtr |
| // (Also need to tell it where it is) |
| int slideOffset = slideRecord.getLastOnDiskOffset(); |
| slideRecord.setLastOnDiskOffset(slideOffset); |
| ptr.addSlideLookup(psrId, slideOffset); |
| logger.log(POILogger.INFO, "New slide/object ended up at " + slideOffset); |
| |
| return psrId; |
| } |
| |
| public MasterSheet<HSLFShape,HSLFTextParagraph> createMasterSheet() throws IOException { |
| // TODO Auto-generated method stub |
| return null; |
| } |
| |
| public Resources getResources() { |
| // TODO Auto-generated method stub |
| return null; |
| } |
| |
| @Override |
| public void close() throws IOException { |
| _hslfSlideShow.close(); |
| } |
| } |