/** * This class is a friendly wrapper on top of the more scary HSLFSlideShow. * * <p>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> { // What we're based on private 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 POILogger logger = POILogFactory.getLogger(this.getClass()); /* =============================================================== * Setup Code * =============================================================== */ /** * 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) { // 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(); } /** Constructs a new, empty, Powerpoint document. */ public HSLFSlideShow() { this(HSLFSlideShowImpl.create()); } /** Constructs a Powerpoint document from an input stream. */ public HSLFSlideShow(InputStream inputStream) throws IOException { this(new HSLFSlideShowImpl(inputStream)); } /** Constructs a Powerpoint document from an POIFSFileSystem. */ public HSLFSlideShow(NPOIFSFileSystem npoifs) throws IOException { this(new HSLFSlideShowImpl(npoifs)); } /** Constructs a Powerpoint document from an DirectoryNode. */ public HSLFSlideShow(DirectoryNode root) throws IOException { this(new HSLFSlideShowImpl(root)); } /** * 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) { Record r = _mostRecentCoreRecords[coreRecordId]; return r; } 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 SlideListWithText masterSLWT = _documentRecord.getMasterSlideListWithText(); SlideListWithText slidesSLWT = _documentRecord.getSlideSlideListWithText(); SlideListWithText notesSLWT = _documentRecord.getNotesSlideListWithText(); // 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. if (masterSLWT != null) { for (SlideAtomsSet sas : masterSLWT.getSlideAtomsSets()) { Record r = getCoreRecordForSAS(sas); int sheetNo = sas.getSlidePersistAtom().getSlideIdentifier(); if (r instanceof org.apache.poi.hslf.record.Slide) { HSLFTitleMaster master = new HSLFTitleMaster((org.apache.poi.hslf.record.Slide) r, sheetNo); master.setSlideShow(this); _titleMasters.add(master); } else if (r instanceof org.apache.poi.hslf.record.MainMaster) { HSLFSlideMaster master = new HSLFSlideMaster((org.apache.poi.hslf.record.MainMaster) r, sheetNo); master.setSlideShow(this); _masters.add(master); } } } // Having sorted out the masters, that leaves the notes and slides // Start by finding the notes records to go with the entries in // notesSLWT org.apache.poi.hslf.record.Notes[] notesRecords; Map<Integer, Integer> slideIdToNotes = new HashMap<Integer, Integer>(); if (notesSLWT == null) { // None notesRecords = new org.apache.poi.hslf.record.Notes[0]; } else { // Match up the records and the SlideAtomSets List<org.apache.poi.hslf.record.Notes> notesRecordsL = new ArrayList<org.apache.poi.hslf.record.Notes>(); 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(); // Ensure it really is a notes record if (r == null || r instanceof org.apache.poi.hslf.record.Notes) { if (r == null) { logger.log( POILogger.WARN, loggerLoc + ", but that record didn't exist - record ignored."); } // we need to add also null-records, otherwise the index references to other existing // don't work anymore org.apache.poi.hslf.record.Notes notesRecord = (org.apache.poi.hslf.record.Notes) r; notesRecordsL.add(notesRecord); // Record the match between slide id and these notes int slideId = spa.getSlideIdentifier(); slideIdToNotes.put(slideId, idx); } else { logger.log(POILogger.ERROR, loggerLoc + ", but that was actually a " + r); } } notesRecords = new org.apache.poi.hslf.record.Notes[notesRecordsL.size()]; notesRecords = notesRecordsL.toArray(notesRecords); } // Now, do the same thing for our slides org.apache.poi.hslf.record.Slide[] slidesRecords; SlideAtomsSet[] slidesSets = new SlideAtomsSet[0]; if (slidesSLWT == null) { // None slidesRecords = new org.apache.poi.hslf.record.Slide[0]; } else { // Match up the records and the SlideAtomSets slidesSets = slidesSLWT.getSlideAtomsSets(); slidesRecords = new org.apache.poi.hslf.record.Slide[slidesSets.length]; for (int i = 0; i < slidesSets.length; i++) { // Get the right core record Record r = getCoreRecordForSAS(slidesSets[i]); // Ensure it really is a slide record if (r instanceof org.apache.poi.hslf.record.Slide) { slidesRecords[i] = (org.apache.poi.hslf.record.Slide) r; } else { logger.log( POILogger.ERROR, "A Slide SlideAtomSet at " + i + " said its record was at refID " + slidesSets[i].getSlidePersistAtom().getRefID() + ", but that was actually a " + r); } } } // Finally, generate model objects for everything // Notes first for (org.apache.poi.hslf.record.Notes n : notesRecords) { HSLFNotes hn = null; if (n != null) { hn = new HSLFNotes(n); hn.setSlideShow(this); } _notes.add(hn); } // Then slides for (int i = 0; i < slidesRecords.length; i++) { SlideAtomsSet sas = slidesSets[i]; int slideIdentifier = sas.getSlidePersistAtom().getSlideIdentifier(); // 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 = slidesRecords[i].getSlideAtom().getNotesID(); if (noteId != 0) { Integer notesPos = slideIdToNotes.get(noteId); if (notesPos != null) { notes = _notes.get(notesPos); } else { logger.log(POILogger.ERROR, "Notes not found for noteId=" + noteId); } } // Now, build our slide HSLFSlide hs = new HSLFSlide(slidesRecords[i], notes, sas, slideIdentifier, (i + 1)); hs.setSlideShow(this); _slides.add(hs); } } @Override public void write(OutputStream out) throws IOException { // check for text paragraph modifications for (HSLFSlide sl : getSlides()) { for (HSLFShape sh : sl.getShapes()) { if (!(sh instanceof HSLFTextShape)) continue; HSLFTextShape hts = (HSLFTextShape) sh; boolean isDirty = false; for (HSLFTextParagraph p : hts.getTextParagraphs()) { isDirty |= p.isDirty(); } if (isDirty) hts.storeText(); } } _hslfSlideShow.write(out); } /* * =============================================================== * Accessor Code * =============================================================== */ /** 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); } /** Return the current page size */ 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); } /** * Change the current page size * * @param pgsize page size (in points) */ 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-ordering Code * =============================================================== */ /** * 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). * * @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 != null) ? removedSlide.getSlideRecord().getSlideAtom().getNotesID() : 0; 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; } /* * =============================================================== * Addition Code * =============================================================== */ /** * Create a blank <code>Slide</code>. * * @return the created <code>Slide</code> */ @Override public HSLFSlide createSlide() { SlideListWithText slist = null; // We need to add the records to the SLWT that deals // with Slides. // Add it, if it doesn't exist 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 org.apache.poi.hslf.record.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); } byte[] uid = HSLFPictureData.getChecksum(data); for (HSLFPictureData pd : getPictureData()) { if (Arrays.equals(pd.getUID(), uid)) { 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); 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 this presentation and returns the associated index. * * @param pict the file containing the image to add * @param format the format of the picture. One of constans defined in the <code>Picture</code> * class. * @return the index to this picture (1 based). */ public HSLFPictureData addPicture(File pict, PictureType format) throws IOException { int length = (int) pict.length(); byte[] data = new byte[length]; FileInputStream is = null; try { is = new FileInputStream(pict); is.read(data); } finally { if (is != null) is.close(); } return addPicture(data, format); } /** * 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() { // detect if this ppt was saved in Office2007 String tag = getSlideMasters().get(0).getProgrammableTag(); boolean ppt2007 = "___PPT12".equals(tag); HeadersFootersContainer hdd = null; for (Record ch : _documentRecord.getChildRecords()) { if (ch instanceof HeadersFootersContainer && ((HeadersFootersContainer) ch).getOptions() == HeadersFootersContainer.SlideHeadersFootersContainer) { hdd = (HeadersFootersContainer) ch; break; } } boolean newRecord = false; if (hdd == null) { hdd = new HeadersFootersContainer(HeadersFootersContainer.SlideHeadersFootersContainer); newRecord = true; } return new HeadersFooters(hdd, this, newRecord, ppt2007); } /** * Return Header / Footer settings for notes * * @return Header / Footer settings for notes */ public HeadersFooters getNotesHeadersFooters() { // detect if this ppt was saved in Office2007 String tag = getSlideMasters().get(0).getProgrammableTag(); boolean ppt2007 = "___PPT12".equals(tag); HeadersFootersContainer hdd = null; for (Record ch : _documentRecord.getChildRecords()) { if (ch instanceof HeadersFootersContainer && ((HeadersFootersContainer) ch).getOptions() == HeadersFootersContainer.NotesHeadersFootersContainer) { hdd = (HeadersFootersContainer) ch; break; } } boolean newRecord = false; if (hdd == null) { hdd = new HeadersFootersContainer(HeadersFootersContainer.NotesHeadersFootersContainer); newRecord = true; } if (ppt2007 && !_notes.isEmpty()) { return new HeadersFooters(hdd, _notes.get(0), newRecord, ppt2007); } return new HeadersFooters(hdd, this, newRecord, ppt2007); } /** * 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 hyperlink to this presentation * * @return 0-based index of the hyperlink */ public int addHyperlink(HSLFHyperlink link) { ExHyperlink ctrl = new ExHyperlink(); ExHyperlinkAtom obj = ctrl.getExHyperlinkAtom(); if (link.getType() == HSLFHyperlink.LINK_SLIDENUMBER) { ctrl.setLinkURL(link.getAddress(), 0x30); } else { ctrl.setLinkURL(link.getAddress()); } ctrl.setLinkTitle(link.getTitle()); int objectId = addToObjListAtom(ctrl); link.setId(objectId); obj.setNumber(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 = (ExObjList) _documentRecord.findFirstOfType(RecordTypes.ExObjList.typeID); if (lst == null) { lst = new ExObjList(); _documentRecord.addChildAfter(lst, _documentRecord.getDocumentAtom()); } 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.Type, PositionDependentRecord> interestingRecords = new HashMap<RecordTypes.Type, 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; } }
/** * A proxy HSSFListener that keeps track of the document formatting records, and provides an easy * way to look up the format strings used by cells from their ids. */ public class FormatTrackingHSSFListener implements HSSFListener { private static POILogger logger = POILogFactory.getLogger(FormatTrackingHSSFListener.class); private final HSSFListener _childListener; private final HSSFDataFormatter _formatter; private final NumberFormat _defaultFormat; private final Map<Integer, FormatRecord> _customFormatRecords = new Hashtable<Integer, FormatRecord>(); private final List<ExtendedFormatRecord> _xfRecords = new ArrayList<ExtendedFormatRecord>(); /** * Creates a format tracking wrapper around the given listener, using the {@link * Locale#getDefault() default locale} for the formats. */ public FormatTrackingHSSFListener(HSSFListener childListener) { this(childListener, LocaleUtil.getUserLocale()); } /** * Creates a format tracking wrapper around the given listener, using the given locale for the * formats. */ public FormatTrackingHSSFListener(HSSFListener childListener, Locale locale) { _childListener = childListener; _formatter = new HSSFDataFormatter(locale); _defaultFormat = NumberFormat.getInstance(locale); } protected int getNumberOfCustomFormats() { return _customFormatRecords.size(); } protected int getNumberOfExtendedFormats() { return _xfRecords.size(); } /** Process this record ourselves, and then pass it on to our child listener */ public void processRecord(Record record) { // Handle it ourselves processRecordInternally(record); // Now pass on to our child _childListener.processRecord(record); } /** * Process the record ourselves, but do not pass it on to the child Listener. * * @param record */ public void processRecordInternally(Record record) { if (record instanceof FormatRecord) { FormatRecord fr = (FormatRecord) record; _customFormatRecords.put(Integer.valueOf(fr.getIndexCode()), fr); } if (record instanceof ExtendedFormatRecord) { ExtendedFormatRecord xr = (ExtendedFormatRecord) record; _xfRecords.add(xr); } } /** * Formats the given numeric of date Cell's contents as a String, in as close as we can to the way * that Excel would do so. Uses the various format records to manage this. * * <p>TODO - move this to a central class in such a way that hssf.usermodel can make use of it too */ public String formatNumberDateCell(CellValueRecordInterface cell) { double value; if (cell instanceof NumberRecord) { value = ((NumberRecord) cell).getValue(); } else if (cell instanceof FormulaRecord) { value = ((FormulaRecord) cell).getValue(); } else { throw new IllegalArgumentException("Unsupported CellValue Record passed in " + cell); } // Get the built in format, if there is one int formatIndex = getFormatIndex(cell); String formatString = getFormatString(cell); if (formatString == null) { return _defaultFormat.format(value); } // Format, using the nice new // HSSFDataFormatter to do the work for us return _formatter.formatRawCellContents(value, formatIndex, formatString); } /** Returns the format string, eg $##.##, for the given number format index. */ public String getFormatString(int formatIndex) { String format = null; if (formatIndex >= HSSFDataFormat.getNumberOfBuiltinBuiltinFormats()) { FormatRecord tfr = _customFormatRecords.get(Integer.valueOf(formatIndex)); if (tfr == null) { logger.log( POILogger.ERROR, "Requested format at index " + formatIndex + ", but it wasn't found"); } else { format = tfr.getFormatString(); } } else { format = HSSFDataFormat.getBuiltinFormat((short) formatIndex); } return format; } /** Returns the format string, eg $##.##, used by your cell */ public String getFormatString(CellValueRecordInterface cell) { int formatIndex = getFormatIndex(cell); if (formatIndex == -1) { // Not found return null; } return getFormatString(formatIndex); } /** Returns the index of the format string, used by your cell, or -1 if none found */ public int getFormatIndex(CellValueRecordInterface cell) { ExtendedFormatRecord xfr = _xfRecords.get(cell.getXFIndex()); if (xfr == null) { logger.log( POILogger.ERROR, "Cell " + cell.getRow() + "," + cell.getColumn() + " uses XF with index " + cell.getXFIndex() + ", but we don't have that"); return -1; } return xfr.getFormatIndex(); } }
/** High level representation of a row of a spreadsheet. */ public class XSSFRow implements Row, Comparable<XSSFRow> { private static final POILogger _logger = POILogFactory.getLogger(XSSFRow.class); /** the xml bean containing all cell definitions for this row */ private final CTRow _row; /** * Cells of this row keyed by their column indexes. The TreeMap ensures that the cells are ordered * by columnIndex in the ascending order. */ private final TreeMap<Integer, XSSFCell> _cells; /** the parent sheet */ private final XSSFSheet _sheet; /** * Construct a XSSFRow. * * @param row the xml bean containing all cell definitions for this row. * @param sheet the parent sheet. */ protected XSSFRow(CTRow row, XSSFSheet sheet) { _row = row; _sheet = sheet; _cells = new TreeMap<Integer, XSSFCell>(); for (CTCell c : row.getCArray()) { XSSFCell cell = new XSSFCell(this, c); _cells.put(cell.getColumnIndex(), cell); sheet.onReadCell(cell); } } /** * Returns the XSSFSheet this row belongs to * * @return the XSSFSheet that owns this row */ public XSSFSheet getSheet() { return this._sheet; } /** * Cell iterator over the physically defined cells: * * <blockquote> * * <pre> * for (Iterator<Cell> it = row.cellIterator(); it.hasNext(); ) { * Cell cell = it.next(); * ... * } * </pre> * * </blockquote> * * @return an iterator over cells in this row. */ public Iterator<Cell> cellIterator() { return (Iterator<Cell>) (Iterator<? extends Cell>) _cells.values().iterator(); } /** * Alias for {@link #cellIterator()} to allow foreach loops: * * <blockquote> * * <pre> * for(Cell cell : row){ * ... * } * </pre> * * </blockquote> * * @return an iterator over cells in this row. */ public Iterator<Cell> iterator() { return cellIterator(); } /** * Compares two <code>XSSFRow</code> objects. Two rows are equal if they belong to the same * worksheet and their row indexes are equal. * * @param row the <code>XSSFRow</code> to be compared. * @return the value <code>0</code> if the row number of this <code>XSSFRow</code> is equal to the * row number of the argument <code>XSSFRow</code>; a value less than <code>0</code> if the * row number of this this <code>XSSFRow</code> is numerically less than the row number of the * argument <code>XSSFRow</code>; and a value greater than <code>0</code> if the row number of * this this <code>XSSFRow</code> is numerically greater than the row number of the argument * <code>XSSFRow</code>. * @throws IllegalArgumentException if the argument row belongs to a different worksheet */ public int compareTo(XSSFRow row) { int thisVal = this.getRowNum(); if (row.getSheet() != getSheet()) throw new IllegalArgumentException("The compared rows must belong to the same XSSFSheet"); int anotherVal = row.getRowNum(); return (thisVal < anotherVal ? -1 : (thisVal == anotherVal ? 0 : 1)); } /** * Use this to create new cells within the row and return it. * * <p>The cell that is returned is a {@link Cell#CELL_TYPE_BLANK}. The type can be changed either * through calling <code>setCellValue</code> or <code>setCellType</code>. * * @param columnIndex - the column number this cell represents * @return Cell a high level representation of the created cell. * @throws IllegalArgumentException if columnIndex < 0 or greater than 16384, the maximum number * of columns supported by the SpreadsheetML format (.xlsx) */ public XSSFCell createCell(int columnIndex) { return createCell(columnIndex, Cell.CELL_TYPE_BLANK); } /** * Use this to create new cells within the row and return it. * * @param columnIndex - the column number this cell represents * @param type - the cell's data type * @return XSSFCell a high level representation of the created cell. * @throws IllegalArgumentException if the specified cell type is invalid, columnIndex < 0 or * greater than 16384, the maximum number of columns supported by the SpreadsheetML format * (.xlsx) * @see Cell#CELL_TYPE_BLANK * @see Cell#CELL_TYPE_BOOLEAN * @see Cell#CELL_TYPE_ERROR * @see Cell#CELL_TYPE_FORMULA * @see Cell#CELL_TYPE_NUMERIC * @see Cell#CELL_TYPE_STRING */ public XSSFCell createCell(int columnIndex, int type) { CTCell ctCell; XSSFCell prev = _cells.get(columnIndex); if (prev != null) { ctCell = prev.getCTCell(); ctCell.set(CTCell.Factory.newInstance()); } else { ctCell = _row.addNewC(); } XSSFCell xcell = new XSSFCell(this, ctCell); xcell.setCellNum(columnIndex); if (type != Cell.CELL_TYPE_BLANK) { xcell.setCellType(type); } _cells.put(columnIndex, xcell); return xcell; } /** * Returns the cell at the given (0 based) index, with the {@link * org.apache.poi.ss.usermodel.Row.MissingCellPolicy} from the parent Workbook. * * @return the cell at the given (0 based) index */ public XSSFCell getCell(int cellnum) { return getCell(cellnum, _sheet.getWorkbook().getMissingCellPolicy()); } /** * Returns the cell at the given (0 based) index, with the specified {@link * org.apache.poi.ss.usermodel.Row.MissingCellPolicy} * * @return the cell at the given (0 based) index * @throws IllegalArgumentException if cellnum < 0 or the specified MissingCellPolicy is invalid * @see Row#RETURN_NULL_AND_BLANK * @see Row#RETURN_BLANK_AS_NULL * @see Row#CREATE_NULL_AS_BLANK */ public XSSFCell getCell(int cellnum, MissingCellPolicy policy) { if (cellnum < 0) throw new IllegalArgumentException("Cell index must be >= 0"); XSSFCell cell = (XSSFCell) _cells.get(cellnum); if (policy == RETURN_NULL_AND_BLANK) { return cell; } if (policy == RETURN_BLANK_AS_NULL) { if (cell == null) return cell; if (cell.getCellType() == Cell.CELL_TYPE_BLANK) { return null; } return cell; } if (policy == CREATE_NULL_AS_BLANK) { if (cell == null) { return createCell((short) cellnum, Cell.CELL_TYPE_BLANK); } return cell; } throw new IllegalArgumentException("Illegal policy " + policy + " (" + policy.id + ")"); } /** * Get the number of the first cell contained in this row. * * @return short representing the first logical cell in the row, or -1 if the row does not contain * any cells. */ public short getFirstCellNum() { return (short) (_cells.size() == 0 ? -1 : _cells.firstKey()); } /** * Gets the index of the last cell contained in this row <b>PLUS ONE</b>. The result also happens * to be the 1-based column number of the last cell. This value can be used as a standard upper * bound when iterating over cells: * * <pre> * short minColIx = row.getFirstCellNum(); * short maxColIx = row.getLastCellNum(); * for(short colIx=minColIx; colIx<maxColIx; colIx++) { * XSSFCell cell = row.getCell(colIx); * if(cell == null) { * continue; * } * //... do something with cell * } * </pre> * * @return short representing the last logical cell in the row <b>PLUS ONE</b>, or -1 if the row * does not contain any cells. */ public short getLastCellNum() { return (short) (_cells.size() == 0 ? -1 : (_cells.lastKey() + 1)); } /** * Get the row's height measured in twips (1/20th of a point). If the height is not set, the * default worksheet value is returned, See {@link * org.apache.poi.xssf.usermodel.XSSFSheet#getDefaultRowHeightInPoints()} * * @return row height measured in twips (1/20th of a point) */ public short getHeight() { return (short) (getHeightInPoints() * 20); } /** * Returns row height measured in point size. If the height is not set, the default worksheet * value is returned, See {@link * org.apache.poi.xssf.usermodel.XSSFSheet#getDefaultRowHeightInPoints()} * * @return row height measured in point size * @see org.apache.poi.xssf.usermodel.XSSFSheet#getDefaultRowHeightInPoints() */ public float getHeightInPoints() { if (this._row.isSetHt()) { return (float) this._row.getHt(); } return _sheet.getDefaultRowHeightInPoints(); } /** * Set the height in "twips" or 1/20th of a point. * * @param height the height in "twips" or 1/20th of a point. <code>-1</code> resets to the default * height */ public void setHeight(short height) { if (height == -1) { if (_row.isSetHt()) _row.unsetHt(); if (_row.isSetCustomHeight()) _row.unsetCustomHeight(); } else { _row.setHt((double) height / 20); _row.setCustomHeight(true); } } /** * Set the row's height in points. * * @param height the height in points. <code>-1</code> resets to the default height */ public void setHeightInPoints(float height) { setHeight((short) (height == -1 ? -1 : (height * 20))); } /** * Gets the number of defined cells (NOT number of cells in the actual row!). That is to say if * only columns 0,4,5 have values then there would be 3. * * @return int representing the number of defined cells in the row. */ public int getPhysicalNumberOfCells() { return _cells.size(); } /** * Get row number this row represents * * @return the row number (0 based) */ public int getRowNum() { return (int) (_row.getR() - 1); } /** * Set the row number of this row. * * @param rowIndex the row number (0-based) * @throws IllegalArgumentException if rowNum < 0 or greater than 1048575 */ public void setRowNum(int rowIndex) { int maxrow = SpreadsheetVersion.EXCEL2007.getLastRowIndex(); if (rowIndex < 0 || rowIndex > maxrow) { throw new IllegalArgumentException( "Invalid row number (" + rowIndex + ") outside allowable range (0.." + maxrow + ")"); } _row.setR(rowIndex + 1); } /** * Get whether or not to display this row with 0 height * * @return - height is zero or not. */ public boolean getZeroHeight() { return this._row.getHidden(); } /** * Set whether or not to display this row with 0 height * * @param height height is zero or not. */ public void setZeroHeight(boolean height) { this._row.setHidden(height); } /** * Remove the Cell from this row. * * @param cell the cell to remove */ public void removeCell(Cell cell) { if (cell.getRow() != this) { throw new IllegalArgumentException("Specified cell does not belong to this row"); } XSSFCell xcell = (XSSFCell) cell; if (xcell.isPartOfArrayFormulaGroup()) { xcell.notifyArrayFormulaChanging(); } _cells.remove(cell.getColumnIndex()); } /** * Returns the underlying CTRow xml bean containing all cell definitions in this row * * @return the underlying CTRow xml bean */ @Internal public CTRow getCTRow() { return _row; } /** * Fired when the document is written to an output stream. * * @see org.apache.poi.xssf.usermodel.XSSFSheet#write(java.io.OutputStream) () */ protected void onDocumentWrite() { // check if cells in the CTRow are ordered boolean isOrdered = true; if (_row.sizeOfCArray() != _cells.size()) isOrdered = false; else { int i = 0; CTCell[] xcell = _row.getCArray(); for (XSSFCell cell : _cells.values()) { CTCell c1 = cell.getCTCell(); CTCell c2 = xcell[i++]; String r1 = c1.getR(); String r2 = c2.getR(); if (!(r1 == null ? r2 == null : r1.equals(r2))) { isOrdered = false; break; } } } if (!isOrdered) { CTCell[] cArray = new CTCell[_cells.size()]; int i = 0; for (XSSFCell c : _cells.values()) { cArray[i++] = c.getCTCell(); } _row.setCArray(cArray); } } /** @return formatted xml representation of this row */ @Override public String toString() { return _row.toString(); } /** * update cell references when shifting rows * * @param n the number of rows to move */ protected void shift(int n) { int rownum = getRowNum() + n; CalculationChain calcChain = _sheet.getWorkbook().getCalculationChain(); int sheetId = (int) _sheet.sheet.getSheetId(); String msg = "Row[rownum=" + getRowNum() + "] contains cell(s) included in a multi-cell array formula. " + "You cannot change part of an array."; for (Cell c : this) { XSSFCell cell = (XSSFCell) c; if (cell.isPartOfArrayFormulaGroup()) { cell.notifyArrayFormulaChanging(msg); } // remove the reference in the calculation chain if (calcChain != null) calcChain.removeItem(sheetId, cell.getReference()); CTCell ctCell = cell.getCTCell(); String r = new CellReference(rownum, cell.getColumnIndex()).formatAsString(); ctCell.setR(r); } setRowNum(rownum); } // 20100915, [email protected]: remove all cells public void removeAllCells() { _cells.clear(); } // 20100915, [email protected]: return cells TreeMap public TreeMap<Integer, XSSFCell> getCells() { return _cells; } }
/** * ftPictFmla (0x0009)<br> * A sub-record within the OBJ record which stores a reference to an object stored in a separate * entry within the OLE2 compound file. * * @author Daniel Noll */ public final class EmbeddedObjectRefSubRecord extends SubRecord implements Cloneable { private static POILogger logger = POILogFactory.getLogger(EmbeddedObjectRefSubRecord.class); public static final short sid = 0x0009; private static final byte[] EMPTY_BYTE_ARRAY = {}; private int field_1_unknown_int; /** either an area or a cell ref */ private Ptg field_2_refPtg; /** for when the 'formula' doesn't parse properly */ private byte[] field_2_unknownFormulaData; /** note- this byte is not present in the encoding if the string length is zero */ private boolean field_3_unicode_flag; // Flags whether the string is Unicode. private String field_4_ole_classname; // Classname of the embedded OLE document (e.g. Word.Document.8) /** * Formulas often have a single non-zero trailing byte. This is in a similar position to he * pre-streamId padding It is unknown if the value is important (it seems to mirror a value a few * bytes earlier) */ private Byte field_4_unknownByte; private Integer field_5_stream_id; // ID of the OLE stream containing the actual data. private byte[] field_6_unknown; // currently for testing only - needs review public EmbeddedObjectRefSubRecord() { field_2_unknownFormulaData = new byte[] { 0x02, 0x6C, 0x6A, 0x16, 0x01, }; // just some sample data. These values vary a lot field_6_unknown = EMPTY_BYTE_ARRAY; field_4_ole_classname = null; } public short getSid() { return sid; } public EmbeddedObjectRefSubRecord(LittleEndianInput in, int size) { // Much guess-work going on here due to lack of any documentation. // See similar source code in OOO: // http://svn.services.openoffice.org/ooo/trunk/sc/source/filter/excel/xiescher.cxx // 1223 void XclImpOleObj::ReadPictFmla( XclImpStream& rStrm, sal_uInt16 nRecSize ) int streamIdOffset = in.readShort(); // OOO calls this 'nFmlaLen' int remaining = size - LittleEndian.SHORT_SIZE; int dataLenAfterFormula = remaining - streamIdOffset; int formulaSize = in.readUShort(); remaining -= LittleEndian.SHORT_SIZE; field_1_unknown_int = in.readInt(); remaining -= LittleEndian.INT_SIZE; byte[] formulaRawBytes = readRawData(in, formulaSize); remaining -= formulaSize; field_2_refPtg = readRefPtg(formulaRawBytes); if (field_2_refPtg == null) { // common case // field_2_n16 seems to be 5 here // The formula almost looks like tTbl but the row/column values seem like garbage. field_2_unknownFormulaData = formulaRawBytes; } else { field_2_unknownFormulaData = null; } int stringByteCount; if (remaining >= dataLenAfterFormula + 3) { int tag = in.readByte(); stringByteCount = LittleEndian.BYTE_SIZE; if (tag != 0x03) { throw new RecordFormatException("Expected byte 0x03 here"); } int nChars = in.readUShort(); stringByteCount += LittleEndian.SHORT_SIZE; if (nChars > 0) { // OOO: the 4th way Xcl stores a unicode string: not even a Grbit byte present if length 0 field_3_unicode_flag = (in.readByte() & 0x01) != 0; stringByteCount += LittleEndian.BYTE_SIZE; if (field_3_unicode_flag) { field_4_ole_classname = StringUtil.readUnicodeLE(in, nChars); stringByteCount += nChars * 2; } else { field_4_ole_classname = StringUtil.readCompressedUnicode(in, nChars); stringByteCount += nChars; } } else { field_4_ole_classname = ""; } } else { field_4_ole_classname = null; stringByteCount = 0; } remaining -= stringByteCount; // Pad to next 2-byte boundary if (((stringByteCount + formulaSize) % 2) != 0) { int b = in.readByte(); remaining -= LittleEndian.BYTE_SIZE; if (field_2_refPtg != null && field_4_ole_classname == null) { field_4_unknownByte = Byte.valueOf((byte) b); } } int nUnexpectedPadding = remaining - dataLenAfterFormula; if (nUnexpectedPadding > 0) { logger.log( POILogger.ERROR, "Discarding " + nUnexpectedPadding + " unexpected padding bytes "); readRawData(in, nUnexpectedPadding); remaining -= nUnexpectedPadding; } // Fetch the stream ID if (dataLenAfterFormula >= 4) { field_5_stream_id = Integer.valueOf(in.readInt()); remaining -= LittleEndian.INT_SIZE; } else { field_5_stream_id = null; } field_6_unknown = readRawData(in, remaining); } private static Ptg readRefPtg(byte[] formulaRawBytes) { LittleEndianInput in = new LittleEndianInputStream(new ByteArrayInputStream(formulaRawBytes)); byte ptgSid = in.readByte(); switch (ptgSid) { case AreaPtg.sid: return new AreaPtg(in); case Area3DPtg.sid: return new Area3DPtg(in); case RefPtg.sid: return new RefPtg(in); case Ref3DPtg.sid: return new Ref3DPtg(in); } return null; } private static byte[] readRawData(LittleEndianInput in, int size) { if (size < 0) { throw new IllegalArgumentException("Negative size (" + size + ")"); } if (size == 0) { return EMPTY_BYTE_ARRAY; } byte[] result = new byte[size]; in.readFully(result); return result; } private int getStreamIDOffset(int formulaSize) { int result = 2 + 4; // formulaSize + f2unknown_int result += formulaSize; int stringLen; if (field_4_ole_classname == null) { // don't write 0x03, stringLen, flag, text stringLen = 0; } else { result += 1 + 2; // 0x03, stringLen stringLen = field_4_ole_classname.length(); if (stringLen > 0) { result += 1; // flag if (field_3_unicode_flag) { result += stringLen * 2; } else { result += stringLen; } } } // pad to next 2 byte boundary if ((result % 2) != 0) { result++; } return result; } private int getDataSize(int idOffset) { int result = 2 + idOffset; // 2 for idOffset short field itself if (field_5_stream_id != null) { result += 4; } return result + field_6_unknown.length; } protected int getDataSize() { int formulaSize = field_2_refPtg == null ? field_2_unknownFormulaData.length : field_2_refPtg.getSize(); int idOffset = getStreamIDOffset(formulaSize); return getDataSize(idOffset); } public void serialize(LittleEndianOutput out) { int formulaSize = field_2_refPtg == null ? field_2_unknownFormulaData.length : field_2_refPtg.getSize(); int idOffset = getStreamIDOffset(formulaSize); int dataSize = getDataSize(idOffset); out.writeShort(sid); out.writeShort(dataSize); out.writeShort(idOffset); out.writeShort(formulaSize); out.writeInt(field_1_unknown_int); int pos = 12; if (field_2_refPtg == null) { out.write(field_2_unknownFormulaData); } else { field_2_refPtg.write(out); } pos += formulaSize; int stringLen; if (field_4_ole_classname == null) { // don't write 0x03, stringLen, flag, text stringLen = 0; } else { out.writeByte(0x03); pos += 1; stringLen = field_4_ole_classname.length(); out.writeShort(stringLen); pos += 2; if (stringLen > 0) { out.writeByte(field_3_unicode_flag ? 0x01 : 0x00); pos += 1; if (field_3_unicode_flag) { StringUtil.putUnicodeLE(field_4_ole_classname, out); pos += stringLen * 2; } else { StringUtil.putCompressedUnicode(field_4_ole_classname, out); pos += stringLen; } } } // pad to next 2-byte boundary (requires 0 or 1 bytes) switch (idOffset - (pos - 6)) { // 6 for 3 shorts: sid, dataSize, idOffset case 1: out.writeByte(field_4_unknownByte == null ? 0x00 : field_4_unknownByte.intValue()); pos++; case 0: break; default: throw new IllegalStateException("Bad padding calculation (" + idOffset + ", " + pos + ")"); } if (field_5_stream_id != null) { out.writeInt(field_5_stream_id.intValue()); pos += 4; } out.write(field_6_unknown); } /** * Gets the stream ID containing the actual data. The data itself can be found under a top-level * directory entry in the OLE2 filesystem under the name "MBD<var>xxxxxxxx</var>" where * <var>xxxxxxxx</var> is this ID converted into hex (in big endian order, funnily enough.) * * @return the data stream ID. Possibly <code>null</code> */ public Integer getStreamId() { return field_5_stream_id; } public String getOLEClassName() { return field_4_ole_classname; } public byte[] getObjectData() { return field_6_unknown; } @Override public EmbeddedObjectRefSubRecord clone() { return this; // TODO proper clone } public String toString() { StringBuffer sb = new StringBuffer(); sb.append("[ftPictFmla]\n"); sb.append(" .f2unknown = ").append(HexDump.intToHex(field_1_unknown_int)).append("\n"); if (field_2_refPtg == null) { sb.append(" .f3unknown = ") .append(HexDump.toHex(field_2_unknownFormulaData)) .append("\n"); } else { sb.append(" .formula = ").append(field_2_refPtg.toString()).append("\n"); } if (field_4_ole_classname != null) { sb.append(" .unicodeFlag = ").append(field_3_unicode_flag).append("\n"); sb.append(" .oleClassname = ").append(field_4_ole_classname).append("\n"); } if (field_4_unknownByte != null) { sb.append(" .f4unknown = ") .append(HexDump.byteToHex(field_4_unknownByte.intValue())) .append("\n"); } if (field_5_stream_id != null) { sb.append(" .streamId = ") .append(HexDump.intToHex(field_5_stream_id.intValue())) .append("\n"); } if (field_6_unknown.length > 0) { sb.append(" .f7unknown = ").append(HexDump.toHex(field_6_unknown)).append("\n"); } sb.append("[/ftPictFmla]"); return sb.toString(); } public void setUnknownFormulaData(byte[] formularData) { field_2_unknownFormulaData = formularData; } public void setOleClassname(String oleClassname) { field_4_ole_classname = oleClassname; } public void setStorageId(int storageId) { field_5_stream_id = storageId; } }
/** * Definition of a special kind of property of some text, or its paragraph. For these properties, a * flag in the "contains" header field tells you the data property family will exist. The value of * the property is itself a mask, encoding several different (but related) properties */ public abstract class BitMaskTextProp extends TextProp implements Cloneable { protected static final POILogger logger = POILogFactory.getLogger(BitMaskTextProp.class); private String[] subPropNames; private int[] subPropMasks; private boolean[] subPropMatches; /** Fetch the list of the names of the sub properties */ public String[] getSubPropNames() { return subPropNames; } /** Fetch the list of if the sub properties match or not */ public boolean[] getSubPropMatches() { return subPropMatches; } protected BitMaskTextProp( int sizeOfDataBlock, int maskInHeader, String overallName, String... subPropNames) { super(sizeOfDataBlock, maskInHeader, "bitmask"); this.subPropNames = subPropNames; this.propName = overallName; subPropMasks = new int[subPropNames.length]; subPropMatches = new boolean[subPropNames.length]; int LSB = Integer.lowestOneBit(maskInHeader); // Initialise the masks list for (int i = 0; i < subPropMasks.length; i++) { subPropMasks[i] = (LSB << i); } } /** Calculate mask from the subPropMatches. */ @Override public int getWriteMask() { /* * The dataValue can't be taken as a mask, as sometimes certain properties * are explicitly set to false, i.e. the mask says the property is defined * but in the actually nibble the property is set to false */ int mask = 0, i = 0; for (int subMask : subPropMasks) { if (subPropMatches[i++]) mask |= subMask; } return mask; } /** * Sets the write mask, i.e. which defines the text properties to be considered * * @param writeMask the mask, bit values outside the property mask range will be ignored */ public void setWriteMask(int writeMask) { int i = 0; for (int subMask : subPropMasks) { subPropMatches[i++] = ((writeMask & subMask) != 0); } } /** * Return the text property value. Clears all bits of the value, which are marked as unset. * * @return the text property value. */ @Override public int getValue() { int val = dataValue, i = 0; ; for (int mask : subPropMasks) { if (!subPropMatches[i++]) { val &= ~mask; } } return val; } /** * Set the value of the text property, and recompute the sub properties based on it, i.e. all * unset subvalues will be cleared. Use {@link #setSubValue(boolean, int)} to explicitly set * subvalues to {@code false}. */ @Override public void setValue(int val) { dataValue = val; // Figure out the values of the sub properties int i = 0; for (int mask : subPropMasks) { subPropMatches[i++] = ((val & mask) != 0); } } /** * Convenience method to set a value with mask, without splitting it into the subvalues * * @param val * @param writeMask */ public void setValueWithMask(int val, int writeMask) { setWriteMask(writeMask); dataValue = val; dataValue = getValue(); if (val != dataValue) { logger.log( POILogger.WARN, "Style properties of '" + getName() + "' don't match mask - output will be sanitized"); if (logger.check(POILogger.DEBUG)) { StringBuilder sb = new StringBuilder( "The following style attributes of the '" + getName() + "' property will be ignored:\n"); int i = 0; for (int mask : subPropMasks) { if (!subPropMatches[i] && (val & mask) != 0) { sb.append(subPropNames[i] + ","); } i++; } logger.log(POILogger.DEBUG, sb.toString()); } } } /** Fetch the true/false status of the subproperty with the given index */ public boolean getSubValue(int idx) { return subPropMatches[idx] && ((dataValue & subPropMasks[idx]) != 0); } /** Set the true/false status of the subproperty with the given index */ public void setSubValue(boolean value, int idx) { subPropMatches[idx] = true; if (value) { dataValue |= subPropMasks[idx]; } else { dataValue &= ~subPropMasks[idx]; } } @Override public BitMaskTextProp clone() { BitMaskTextProp newObj = (BitMaskTextProp) super.clone(); // Don't carry over matches, but keep everything // else as it was newObj.subPropMatches = new boolean[subPropMatches.length]; return newObj; } public BitMaskTextProp cloneAll() { return (BitMaskTextProp) super.clone(); } }
/** * Converts Word files (95-2007) into HTML files. * * <p>This implementation doesn't create images or links to them. This can be changed by overriding * {@link #processImage(Element, boolean, Picture)} method. * * @author Sergey Vladimirov (vlsergey {at} gmail {dot} com) */ public class DocConverter extends CustomerAbsConverter { /** * test * * @param args */ public static void main(String[] args) { String filePath = "c:\\poi\\test.doc"; // String filePath = "c:\\poi\\××××内部文档保密系统20120425 v4.1 [秘密]1.doc"; String output = "c:/poi/output/test.html"; DocConverter.convert(filePath, output); } /** 文档图片集 */ PicturesTable picstab = null; /** added pageContainer */ static Element pageContainer = null; private static String output; private static String fileName; /** 转换文档 */ public static String convert(String filePath, String output) { HtmlDocumentFacade facade = null; // File docFile = new File(docPath); try { facade = new HtmlDocumentFacade( DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument()); String s1 = "background-color:gray"; facade.addStyleClass(facade.body, "body", s1); } catch (Exception e) { e.printStackTrace(); } if (AbstractWordUtils.isEmpty(filePath) || AbstractWordUtils.isEmpty(output)) { System.err.println("docPath OR output is empty. >>!quit"); return ""; } System.out.println("Converting " + filePath); System.out.println("Saving output to " + output); DocConverter.output = output .substring(0, output.lastIndexOf(".html")) .concat(File.separator) .concat("images") .concat(File.separator); // get fileName new File(DocConverter.output).mkdirs(); try { Document doc = null; if (facade == null) doc = DocConverter.process(new File(filePath)); else { Document document = facade.getDocument(); Element window = document.createElement("div"); Element center = document.createElement("center"); center.appendChild(window); facade.addStyleClass( window, "window", "border:2px solid green;width:800px!important; margin:0 auto; background-color:#fff; text-align:left;"); facade.body.appendChild(center); pageContainer = window; doc = DocConverter.process(new File(filePath), facade); } FileWriter out = new FileWriter(output); DOMSource domSource = new DOMSource(doc); StreamResult streamResult = new StreamResult(out); TransformerFactory tf = TransformerFactory.newInstance(); Transformer serializer = tf.newTransformer(); // TODO set encoding from a command argument serializer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); serializer.setOutputProperty(OutputKeys.INDENT, "yes"); serializer.setOutputProperty(OutputKeys.METHOD, "html"); serializer.transform(domSource, streamResult); out.close(); } catch (Exception e) { e.printStackTrace(); } return ""; } static Document process(File docFile) throws Exception { final HWPFDocument wordDocument = new HWPFDocument(new FileInputStream(docFile)); DocConverter converter = new DocConverter(DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument()); converter.processDocument(wordDocument); return converter.getDocument(); } static Document process(File docFile, HtmlDocumentFacade htmlDocumentFacade) throws Exception { final HWPFDocument wordDocument = new HWPFDocument(new FileInputStream(docFile)); DocConverter converter = new DocConverter(htmlDocumentFacade); converter.processDocument(wordDocument); return converter.getDocument(); } @Override public void processDocument(HWPFDocument wordDocument) { if (wordDocument.getPicturesTable().getAllPictures().size() > 0) { this.picstab = wordDocument.getPicturesTable(); } super.processDocument(wordDocument); } /** * Holds properties values, applied to current <tt>p</tt> element. Those properties shall not be * doubled in children <tt>span</tt> elements. */ private static class BlockProperies { final String pFontName; final int pFontSize; public BlockProperies(String pFontName, int pFontSize) { this.pFontName = pFontName; this.pFontSize = pFontSize; } } private static final POILogger logger = POILogFactory.getLogger(DocConverter.class); private static String getSectionStyle(Section section) { float leftMargin = section.getMarginLeft() / TWIPS_PER_INCH; float rightMargin = section.getMarginRight() / TWIPS_PER_INCH; float topMargin = section.getMarginTop() / TWIPS_PER_INCH; float bottomMargin = section.getMarginBottom() / TWIPS_PER_INCH; String style = "margin: " + topMargin + "in " + rightMargin + "in " + bottomMargin + "in " + leftMargin + "in;"; if (section.getNumColumns() > 1) { style += "column-count: " + (section.getNumColumns()) + ";"; if (section.isColumnsEvenlySpaced()) { float distance = section.getDistanceBetweenColumns() / TWIPS_PER_INCH; style += "column-gap: " + distance + "in;"; } else { style += "column-gap: 0.25in;"; } } return style; } private final Stack<BlockProperies> blocksProperies = new Stack<BlockProperies>(); private final HtmlDocumentFacade htmlDocumentFacade; private Element notes = null; /** * Creates new instance of {@link DocConverter}. Can be used for output several {@link * HWPFDocument}s into single HTML document. * * @param document XML DOM Document used as HTML document */ public DocConverter(Document document) { this.htmlDocumentFacade = new HtmlDocumentFacade(document); } public DocConverter(HtmlDocumentFacade htmlDocumentFacade) { this.htmlDocumentFacade = htmlDocumentFacade; } @Override protected void afterProcess() { if (notes != null) htmlDocumentFacade.getBody().appendChild(notes); htmlDocumentFacade.updateStylesheet(); } public Document getDocument() { return htmlDocumentFacade.getDocument(); } @Override protected void outputCharacters(Element pElement, CharacterRun characterRun, String text) { Element span = htmlDocumentFacade.document.createElement("span"); pElement.appendChild(span); StringBuilder style = new StringBuilder(); BlockProperies blockProperies = this.blocksProperies.peek(); Triplet triplet = getCharacterRunTriplet(characterRun); if (WordToHtmlUtils.isNotEmpty(triplet.fontName) && !WordToHtmlUtils.equals(triplet.fontName, blockProperies.pFontName)) { style.append("font-family:" + triplet.fontName + ";"); } if (characterRun.getFontSize() / 2 != blockProperies.pFontSize) { style.append("font-size:" + characterRun.getFontSize() / 2 + "pt;"); } if (triplet.bold) { style.append("font-weight:bold;"); } if (triplet.italic) { style.append("font-style:italic;"); } WordToHtmlUtils.addCharactersProperties(characterRun, style); if (style.length() != 0) htmlDocumentFacade.addStyleClass(span, "s", style.toString()); Text textNode = htmlDocumentFacade.createText(text); span.appendChild(textNode); } @Override protected void processBookmarks( HWPFDocument wordDocument, Element currentBlock, Range range, int currentTableLevel, List<Bookmark> rangeBookmarks) { Element parent = currentBlock; for (Bookmark bookmark : rangeBookmarks) { Element bookmarkElement = htmlDocumentFacade.createBookmark(bookmark.getName()); parent.appendChild(bookmarkElement); parent = bookmarkElement; } if (range != null) processCharacters(wordDocument, currentTableLevel, range, parent); } @Override protected void processDocumentInformation(SummaryInformation summaryInformation) { if (WordToHtmlUtils.isNotEmpty(summaryInformation.getTitle())) htmlDocumentFacade.setTitle(summaryInformation.getTitle()); if (WordToHtmlUtils.isNotEmpty(summaryInformation.getAuthor())) htmlDocumentFacade.addAuthor(summaryInformation.getAuthor()); if (WordToHtmlUtils.isNotEmpty(summaryInformation.getKeywords())) htmlDocumentFacade.addKeywords(summaryInformation.getKeywords()); if (WordToHtmlUtils.isNotEmpty(summaryInformation.getComments())) htmlDocumentFacade.addDescription(summaryInformation.getComments()); } @Override public void processDocumentPart(HWPFDocument wordDocument, Range range) { super.processDocumentPart(wordDocument, range); afterProcess(); } @Override protected void processDrawnObject( HWPFDocument doc, CharacterRun characterRun, OfficeDrawing officeDrawing, String path, Element block) { Element img = htmlDocumentFacade.createImage(path); block.appendChild(img); } @Override protected void processEndnoteAutonumbered( HWPFDocument wordDocument, int noteIndex, Element block, Range endnoteTextRange) { processNoteAutonumbered(wordDocument, "end", noteIndex, block, endnoteTextRange); } @Override protected void processFootnoteAutonumbered( HWPFDocument wordDocument, int noteIndex, Element block, Range footnoteTextRange) { processNoteAutonumbered(wordDocument, "foot", noteIndex, block, footnoteTextRange); } @Override protected void processHyperlink( HWPFDocument wordDocument, Element currentBlock, Range textRange, int currentTableLevel, String hyperlink) { Element basicLink = htmlDocumentFacade.createHyperlink(hyperlink); currentBlock.appendChild(basicLink); if (textRange != null) processCharacters(wordDocument, currentTableLevel, textRange, basicLink); } @SuppressWarnings("deprecation") protected void processImage( Element currentBlock, boolean inlined, Picture picture, String imageSourcePath) { final int aspectRatioX = picture.getHorizontalScalingFactor(); final int aspectRatioY = picture.getVerticalScalingFactor(); StringBuilder style = new StringBuilder(); final float imageWidth; final float imageHeight; final float cropTop; final float cropBottom; final float cropLeft; final float cropRight; if (aspectRatioX > 0) { imageWidth = picture.getDxaGoal() * aspectRatioX / 1000 / TWIPS_PER_INCH; cropRight = picture.getDxaCropRight() * aspectRatioX / 1000 / TWIPS_PER_INCH; cropLeft = picture.getDxaCropLeft() * aspectRatioX / 1000 / TWIPS_PER_INCH; } else { imageWidth = picture.getDxaGoal() / TWIPS_PER_INCH; cropRight = picture.getDxaCropRight() / TWIPS_PER_INCH; cropLeft = picture.getDxaCropLeft() / TWIPS_PER_INCH; } if (aspectRatioY > 0) { imageHeight = picture.getDyaGoal() * aspectRatioY / 1000 / TWIPS_PER_INCH; cropTop = picture.getDyaCropTop() * aspectRatioY / 1000 / TWIPS_PER_INCH; cropBottom = picture.getDyaCropBottom() * aspectRatioY / 1000 / TWIPS_PER_INCH; } else { imageHeight = picture.getDyaGoal() / TWIPS_PER_INCH; cropTop = picture.getDyaCropTop() / TWIPS_PER_INCH; cropBottom = picture.getDyaCropBottom() / TWIPS_PER_INCH; } Element root; if (cropTop != 0 || cropRight != 0 || cropBottom != 0 || cropLeft != 0) { float visibleWidth = Math.max(0, imageWidth - cropLeft - cropRight); float visibleHeight = Math.max(0, imageHeight - cropTop - cropBottom); root = htmlDocumentFacade.createBlock(); htmlDocumentFacade.addStyleClass( root, "d", "vertical-align:text-bottom;width:" + visibleWidth + "in;height:" + visibleHeight + "in;"); // complex Element inner = htmlDocumentFacade.createBlock(); htmlDocumentFacade.addStyleClass( inner, "d", "position:relative;width:" + visibleWidth + "in;height:" + visibleHeight + "in;overflow:hidden;"); root.appendChild(inner); Element image = htmlDocumentFacade.createImage(imageSourcePath); htmlDocumentFacade.addStyleClass( image, "i", "position:absolute;left:-" + cropLeft + ";top:-" + cropTop + ";width:" + imageWidth + "in;height:" + imageHeight + "in;"); inner.appendChild(image); style.append("overflow:hidden;"); } else { root = htmlDocumentFacade.createImage(imageSourcePath); root.setAttribute( "style", "width:" + imageWidth + "in;height:" + imageHeight + "in;vertical-align:text-bottom;"); } currentBlock.appendChild(root); } @Override protected void processImageWithoutPicturesManager( Element currentBlock, boolean inlined, Picture picture) { // no default implementation -- skip currentBlock.appendChild( htmlDocumentFacade.document.createComment( "Image link to '" + picture.suggestFullFileName() + "' can be here")); } @Override protected void processLineBreak(Element block, CharacterRun characterRun) { block.appendChild(htmlDocumentFacade.createLineBreak()); } protected void processNoteAutonumbered( HWPFDocument doc, String type, int noteIndex, Element block, Range noteTextRange) { final String textIndex = String.valueOf(noteIndex + 1); final String textIndexClass = htmlDocumentFacade.getOrCreateCssClass("a", "vertical-align:super;font-size:smaller;"); final String forwardNoteLink = type + "note_" + textIndex; final String backwardNoteLink = type + "note_back_" + textIndex; Element anchor = htmlDocumentFacade.createHyperlink("#" + forwardNoteLink); anchor.setAttribute("name", backwardNoteLink); anchor.setAttribute("class", textIndexClass + " " + type + "noteanchor"); anchor.setTextContent(textIndex); block.appendChild(anchor); if (notes == null) { notes = htmlDocumentFacade.createBlock(); notes.setAttribute("class", "notes"); } Element note = htmlDocumentFacade.createBlock(); note.setAttribute("class", type + "note"); notes.appendChild(note); Element bookmark = htmlDocumentFacade.createBookmark(forwardNoteLink); bookmark.setAttribute("href", "#" + backwardNoteLink); bookmark.setTextContent(textIndex); bookmark.setAttribute("class", textIndexClass + " " + type + "noteindex"); note.appendChild(bookmark); note.appendChild(htmlDocumentFacade.createText(" ")); Element span = htmlDocumentFacade.getDocument().createElement("span"); span.setAttribute("class", type + "notetext"); note.appendChild(span); this.blocksProperies.add(new BlockProperies("", -1)); try { processCharacters(doc, Integer.MIN_VALUE, noteTextRange, span); } finally { this.blocksProperies.pop(); } } @Override protected void processPageBreak(HWPFDocument wordDocument, Element flow) { flow.appendChild(htmlDocumentFacade.createLineBreak()); } protected void processPageref( HWPFDocument hwpfDocument, Element currentBlock, Range textRange, int currentTableLevel, String pageref) { Element basicLink = htmlDocumentFacade.createHyperlink("#" + pageref); currentBlock.appendChild(basicLink); if (textRange != null) processCharacters(hwpfDocument, currentTableLevel, textRange, basicLink); } protected void processParagraph( HWPFDocument hwpfDocument, Element parentElement, int currentTableLevel, Paragraph paragraph, String bulletText) { final Element pElement = htmlDocumentFacade.createParagraph(); parentElement.appendChild(pElement); /*if(itemSymbol) System.out.println(itemSymbol);*/ if (itemSymbol) { Element span = htmlDocumentFacade.getDocument().createElement("span"); htmlDocumentFacade.addStyleClass( span, "itemSymbol", "font-size:12.0pt;line-height:150%;font-family:Wingdings;mso-ascii-font-family:Wingdings;mso-hide:none;mso-ansi-language:EN-US;mso-fareast-language:ZH-CN;font-weight:normal;mso-bidi-font-weight:normal;font-style:normal;mso-bidi-font-style:normal;text-underline:windowtext none;text-decoration:none;background:transparent"); span.setTextContent("Ø"); pElement.appendChild(span); itemSymbol = false; } StringBuilder style = new StringBuilder(); WordToHtmlUtils.addParagraphProperties(paragraph, style); final int charRuns = paragraph.numCharacterRuns(); if (charRuns == 0) { return; } { final String pFontName; final int pFontSize; final CharacterRun characterRun = paragraph.getCharacterRun(0); if ("".equals(paragraph.text().trim())) { pElement.setTextContent(String.valueOf(UNICODECHAR_NO_BREAK_SPACE)); } if (characterRun != null) { Triplet triplet = getCharacterRunTriplet(characterRun); pFontSize = characterRun.getFontSize() / 2; pFontName = triplet.fontName; WordToHtmlUtils.addFontFamily(pFontName, style); WordToHtmlUtils.addFontSize(pFontSize, style); } else { pFontSize = -1; pFontName = WordToHtmlUtils.EMPTY; } blocksProperies.push(new BlockProperies(pFontName, pFontSize)); } try { if (WordToHtmlUtils.isNotEmpty(bulletText)) { if (bulletText.endsWith("\t")) { /* * We don't know how to handle all cases in HTML, but at * least simplest case shall be handled */ final float defaultTab = TWIPS_PER_INCH / 2; float firstLinePosition = paragraph.getIndentFromLeft() + paragraph.getFirstLineIndent() + 20; // char have // some space float nextStop = (float) (Math.ceil(firstLinePosition / defaultTab) * defaultTab); final float spanMinWidth = nextStop - firstLinePosition; Element span = htmlDocumentFacade.getDocument().createElement("span"); htmlDocumentFacade.addStyleClass( span, "s", "display: inline-block; text-indent: 0; min-width: " + (spanMinWidth / TWIPS_PER_INCH) + "in;"); pElement.appendChild(span); Text textNode = htmlDocumentFacade.createText( bulletText.substring(0, bulletText.length() - 1) + UNICODECHAR_ZERO_WIDTH_SPACE + UNICODECHAR_NO_BREAK_SPACE); span.appendChild(textNode); } else { Text textNode = htmlDocumentFacade.createText(bulletText.substring(0, bulletText.length() - 1)); pElement.appendChild(textNode); } } processCharacters(hwpfDocument, currentTableLevel, paragraph, pElement); } finally { blocksProperies.pop(); } if (style.length() > 0) htmlDocumentFacade.addStyleClass(pElement, "p", style.toString()); WordToHtmlUtils.compactSpans(pElement); return; } protected void processSection(HWPFDocument wordDocument, Section section, int sectionCounter) { // TODO 解析章节 Element div = htmlDocumentFacade.createBlock(); htmlDocumentFacade.addStyleClass(div, "d", getSectionStyle(section)); // htmlDocumentFacade.body.appendChild( div ); pageContainer.appendChild(div); processParagraphes(wordDocument, div, section, Integer.MIN_VALUE); } protected void processSingleSection(HWPFDocument wordDocument, Section section) { htmlDocumentFacade.addStyleClass(htmlDocumentFacade.body, "b", getSectionStyle(section)); processParagraphes(wordDocument, htmlDocumentFacade.body, section, Integer.MIN_VALUE); } /** 解析table */ protected void processTable(HWPFDocument hwpfDocument, Element flow, Table table) { Element tableHeader = htmlDocumentFacade.createTableHeader(); Element tableBody = htmlDocumentFacade.createTableBody(); final int[] tableCellEdges = WordToHtmlUtils.buildTableCellEdgesArray(table); final int tableRows = table.numRows(); int maxColumns = Integer.MIN_VALUE; for (int r = 0; r < tableRows; r++) { maxColumns = Math.max(maxColumns, table.getRow(r).numCells()); } for (int r = 0; r < tableRows; r++) { TableRow tableRow = table.getRow(r); Element tableRowElement = htmlDocumentFacade.createTableRow(); StringBuilder tableRowStyle = new StringBuilder(); WordToHtmlUtils.addTableRowProperties(tableRow, tableRowStyle); // index of current element in tableCellEdges[] int currentEdgeIndex = 0; final int rowCells = tableRow.numCells(); for (int c = 0; c < rowCells; c++) { TableCell tableCell = tableRow.getCell(c); if (tableCell.isVerticallyMerged() && !tableCell.isFirstVerticallyMerged()) { currentEdgeIndex += getNumberColumnsSpanned(tableCellEdges, currentEdgeIndex, tableCell); continue; } Element tableCellElement; if (tableRow.isTableHeader()) { tableCellElement = htmlDocumentFacade.createTableHeaderCell(); } else { tableCellElement = htmlDocumentFacade.createTableCell(); } StringBuilder tableCellStyle = new StringBuilder(); WordToHtmlUtils.addTableCellProperties( tableRow, tableCell, r == 0, r == tableRows - 1, c == 0, c == rowCells - 1, tableCellStyle); int colSpan = getNumberColumnsSpanned(tableCellEdges, currentEdgeIndex, tableCell); currentEdgeIndex += colSpan; if (colSpan == 0) continue; if (colSpan != 1) tableCellElement.setAttribute("colspan", String.valueOf(colSpan)); final int rowSpan = getNumberRowsSpanned(table, tableCellEdges, r, c, tableCell); if (rowSpan > 1) tableCellElement.setAttribute("rowspan", String.valueOf(rowSpan)); processParagraphes(hwpfDocument, tableCellElement, tableCell, table.getTableLevel()); if (!tableCellElement.hasChildNodes()) { tableCellElement.appendChild(htmlDocumentFacade.createParagraph()); } if (tableCellStyle.length() > 0) htmlDocumentFacade.addStyleClass( tableCellElement, tableCellElement.getTagName(), tableCellStyle.toString()); tableRowElement.appendChild(tableCellElement); } if (tableRowStyle.length() > 0) tableRowElement.setAttribute( "class", htmlDocumentFacade.getOrCreateCssClass("r", tableRowStyle.toString())); if (tableRow.isTableHeader()) { tableHeader.appendChild(tableRowElement); } else { tableBody.appendChild(tableRowElement); } } final Element tableElement = htmlDocumentFacade.createTable(); tableElement.setAttribute( "class", htmlDocumentFacade.getOrCreateCssClass( "t", "table-layout:fixed;border-collapse:collapse;border-spacing:0;")); if (tableHeader.hasChildNodes()) { tableElement.appendChild(tableHeader); } if (tableBody.hasChildNodes()) { tableElement.appendChild(tableBody); flow.appendChild(tableElement); } else { logger.log( POILogger.WARN, "Table without body starting at [", Integer.valueOf(table.getStartOffset()), "; ", Integer.valueOf(table.getEndOffset()), ")"); } } /** 加入图片 */ protected void processImage(Element parent, CharacterRun cr) { if (this.picstab.hasPicture(cr)) { Picture pic = picstab.extractPicture(cr, false); String fileName = pic.suggestFullFileName(); OutputStream out = null; try { // TWIPS_PER_INCH/ out = new FileOutputStream(new File(output.concat(File.separator).concat(fileName))); pic.writeImageContent(out); Element img = htmlDocumentFacade.getDocument().createElement("img"); String uri = fileName.concat("image").concat(File.separator).concat(fileName); img.setAttribute("src", uri); if (pic.getWidth() > 600) img.setAttribute("style", "width: 600px;"); Element imgBlock = htmlDocumentFacade.createBlock(); this.htmlDocumentFacade.addStyleClass(imgBlock, "imgs", "text-align:center;"); imgBlock.appendChild(img); parent.appendChild(imgBlock); } catch (Exception e) { e.printStackTrace(); } finally { if (out != null) try { out.close(); } catch (IOException e) { e.printStackTrace(); } } } } // 图片END private boolean itemSymbol = false; @Override protected void processParagraphes( HWPFDocument wordDocument, Element flow, Range range, int currentTableLevel) { // TODO mc process paragraphes final ListTables listTables = wordDocument.getListTables(); int currentListInfo = 0; final int paragraphs = range.numParagraphs(); for (int p = 0; p < paragraphs; p++) { Paragraph paragraph = range.getParagraph(p); // 加入图片 CharacterRun cr = paragraph.getCharacterRun(0); this.processImage(flow, cr); // table if (paragraph.isInTable() && paragraph.getTableLevel() != currentTableLevel) { if (paragraph.getTableLevel() < currentTableLevel) throw new IllegalStateException( "Trying to process table cell with higher level (" + paragraph.getTableLevel() + ") than current table level (" + currentTableLevel + ") as inner table part"); Table table = range.getTable(paragraph); processTable(wordDocument, flow, table); p += table.numParagraphs(); p--; continue; } // 换页 if (paragraph.text().equals("\u000c")) { processPageBreak(wordDocument, flow); } if (paragraph.getIlfo() != currentListInfo) { currentListInfo = paragraph.getIlfo(); } // 嵌套段落 if (currentListInfo != 0) { if (listTables != null) { final ListFormatOverride listFormatOverride = listTables.getOverride(paragraph.getIlfo()); String label = getBulletText(listTables, paragraph, listFormatOverride.getLsid()); if ("".equals(label)) { itemSymbol = true; /* Element span = htmlDocumentFacade.getDocument().createElement("span"); span.setAttribute("style", "font-size:12.0pt;line-height:150%;font-family:Wingdings;mso-ascii-font-family:Wingdings;mso-hide:none;mso-ansi-language:EN-US;mso-fareast-language:ZH-CN;font-weight:normal;mso-bidi-font-weight:normal;font-style:normal;mso-bidi-font-style:normal;text-underline:windowtext none;text-decoration:none;background:transparent"); span.setTextContent("Ø"); flow.appendChild(span); */ } processParagraph(wordDocument, flow, currentTableLevel, paragraph, label); } else { logger.log( POILogger.WARN, "Paragraph #" + paragraph.getStartOffset() + "-" + paragraph.getEndOffset() + " has reference to list structure #" + currentListInfo + ", but listTables not defined in file"); processParagraph( wordDocument, flow, currentTableLevel, paragraph, AbstractWordUtils.EMPTY); } } else { processParagraph(wordDocument, flow, currentTableLevel, paragraph, AbstractWordUtils.EMPTY); } } } /////////////// private static String getBulletText(ListTables listTables, Paragraph paragraph, int listId) { final ListLevel listLevel = listTables.getLevel(listId, paragraph.getIlvl()); if (listLevel.getNumberText() == null) return ""; StringBuffer bulletBuffer = new StringBuffer(); char[] xst = listLevel.getNumberText().toCharArray(); for (char element : xst) { if (element < 9) { ListLevel numLevel = listTables.getLevel(listId, element); int num = numLevel.getStartAt(); bulletBuffer.append(NumberFormatter.getNumber(num, listLevel.getNumberFormat())); if (numLevel == listLevel) { numLevel.setStartAt(numLevel.getStartAt() + 1); } } else { bulletBuffer.append(element); } } byte follow = listLevel.getTypeOfCharFollowingTheNumber(); switch (follow) { case 0: bulletBuffer.append("\t"); break; case 1: bulletBuffer.append(" "); break; default: break; } return bulletBuffer.toString(); } }
/** * Helper for part and pack URI. * * @author Julien Chable, CDubet, Kim Ung * @version 0.1 */ public final class PackagingURIHelper { private static final POILogger _logger = POILogFactory.getLogger(PackagingURIHelper.class); /** Package root URI. */ private static URI packageRootUri; /** Extension name of a relationship part. */ public static final String RELATIONSHIP_PART_EXTENSION_NAME; /** Segment name of a relationship part. */ public static final String RELATIONSHIP_PART_SEGMENT_NAME; /** Segment name of the package properties folder. */ public static final String PACKAGE_PROPERTIES_SEGMENT_NAME; /** Core package properties art name. */ public static final String PACKAGE_CORE_PROPERTIES_NAME; /** Forward slash URI separator. */ public static final char FORWARD_SLASH_CHAR; /** Forward slash URI separator. */ public static final String FORWARD_SLASH_STRING; /** Package relationships part URI */ public static final URI PACKAGE_RELATIONSHIPS_ROOT_URI; /** Package relationships part name. */ public static final PackagePartName PACKAGE_RELATIONSHIPS_ROOT_PART_NAME; /** Core properties part URI. */ public static final URI CORE_PROPERTIES_URI; /** Core properties partname. */ public static final PackagePartName CORE_PROPERTIES_PART_NAME; /** Root package URI. */ public static final URI PACKAGE_ROOT_URI; /** Root package part name. */ public static final PackagePartName PACKAGE_ROOT_PART_NAME; /* Static initialization */ static { RELATIONSHIP_PART_SEGMENT_NAME = "_rels"; RELATIONSHIP_PART_EXTENSION_NAME = ".rels"; FORWARD_SLASH_CHAR = '/'; FORWARD_SLASH_STRING = "/"; PACKAGE_PROPERTIES_SEGMENT_NAME = "docProps"; PACKAGE_CORE_PROPERTIES_NAME = "core.xml"; // Make URI URI uriPACKAGE_ROOT_URI = null; URI uriPACKAGE_RELATIONSHIPS_ROOT_URI = null; URI uriPACKAGE_PROPERTIES_URI = null; try { uriPACKAGE_ROOT_URI = new URI("/"); uriPACKAGE_RELATIONSHIPS_ROOT_URI = new URI( FORWARD_SLASH_CHAR + RELATIONSHIP_PART_SEGMENT_NAME + FORWARD_SLASH_CHAR + RELATIONSHIP_PART_EXTENSION_NAME); packageRootUri = new URI("/"); uriPACKAGE_PROPERTIES_URI = new URI( FORWARD_SLASH_CHAR + PACKAGE_PROPERTIES_SEGMENT_NAME + FORWARD_SLASH_CHAR + PACKAGE_CORE_PROPERTIES_NAME); } catch (URISyntaxException e) { // Should never happen in production as all data are fixed } PACKAGE_ROOT_URI = uriPACKAGE_ROOT_URI; PACKAGE_RELATIONSHIPS_ROOT_URI = uriPACKAGE_RELATIONSHIPS_ROOT_URI; CORE_PROPERTIES_URI = uriPACKAGE_PROPERTIES_URI; // Make part name from previous URI PackagePartName tmpPACKAGE_ROOT_PART_NAME = null; PackagePartName tmpPACKAGE_RELATIONSHIPS_ROOT_PART_NAME = null; PackagePartName tmpCORE_PROPERTIES_URI = null; try { tmpPACKAGE_RELATIONSHIPS_ROOT_PART_NAME = createPartName(PACKAGE_RELATIONSHIPS_ROOT_URI); tmpCORE_PROPERTIES_URI = createPartName(CORE_PROPERTIES_URI); tmpPACKAGE_ROOT_PART_NAME = new PackagePartName(PACKAGE_ROOT_URI, false); } catch (InvalidFormatException e) { // Should never happen in production as all data are fixed } PACKAGE_RELATIONSHIPS_ROOT_PART_NAME = tmpPACKAGE_RELATIONSHIPS_ROOT_PART_NAME; CORE_PROPERTIES_PART_NAME = tmpCORE_PROPERTIES_URI; PACKAGE_ROOT_PART_NAME = tmpPACKAGE_ROOT_PART_NAME; } /** * Gets the URI for the package root. * * @return URI of the package root. */ public static URI getPackageRootUri() { return packageRootUri; } /** * Know if the specified URI is a relationship part name. * * @param partUri URI to check. * @return <i>true</i> if the URI <i>false</i>. */ public static boolean isRelationshipPartURI(URI partUri) { if (partUri == null) throw new IllegalArgumentException("partUri"); return partUri .getPath() .matches( ".*" + RELATIONSHIP_PART_SEGMENT_NAME + ".*" + RELATIONSHIP_PART_EXTENSION_NAME + "$"); } /** Get file name from the specified URI. */ public static String getFilename(URI uri) { if (uri != null) { String path = uri.getPath(); int len = path.length(); int num2 = len; while (--num2 >= 0) { char ch1 = path.charAt(num2); if (ch1 == PackagingURIHelper.FORWARD_SLASH_CHAR) return path.substring(num2 + 1, len); } } return ""; } /** Get the file name without the trailing extension. */ public static String getFilenameWithoutExtension(URI uri) { String filename = getFilename(uri); int dotIndex = filename.lastIndexOf("."); if (dotIndex == -1) return filename; return filename.substring(0, dotIndex); } /** Get the directory path from the specified URI. */ public static URI getPath(URI uri) { if (uri != null) { String path = uri.getPath(); int len = path.length(); int num2 = len; while (--num2 >= 0) { char ch1 = path.charAt(num2); if (ch1 == PackagingURIHelper.FORWARD_SLASH_CHAR) { try { return new URI(path.substring(0, num2)); } catch (URISyntaxException e) { return null; } } } } return null; } /** * Combine two URIs. * * @param prefix the prefix URI * @param suffix the suffix URI * @return the combined URI */ public static URI combine(URI prefix, URI suffix) { URI retUri = null; try { retUri = new URI(combine(prefix.getPath(), suffix.getPath())); } catch (URISyntaxException e) { throw new IllegalArgumentException("Prefix and suffix can't be combine !"); } return retUri; } /** Combine a string URI with a prefix and a suffix. */ public static String combine(String prefix, String suffix) { if (!prefix.endsWith("" + FORWARD_SLASH_CHAR) && !suffix.startsWith("" + FORWARD_SLASH_CHAR)) return prefix + FORWARD_SLASH_CHAR + suffix; else if ((!prefix.endsWith("" + FORWARD_SLASH_CHAR) && suffix.startsWith("" + FORWARD_SLASH_CHAR) || (prefix.endsWith("" + FORWARD_SLASH_CHAR) && !suffix.startsWith("" + FORWARD_SLASH_CHAR)))) return prefix + suffix; else return ""; } /** * Fully relativize the source part URI against the target part URI. * * @param sourceURI The source part URI. * @param targetURI The target part URI. * @param msCompatible if true then remove leading slash from the relativized URI. This flag * violates [M1.4]: A part name shall start with a forward slash ('/') character, but allows * generating URIs compatible with MS Office and OpenOffice. * @return A fully relativize part name URI ('word/media/image1.gif', '/word/document.xml' => * 'media/image1.gif') else <code>null</code>. */ public static URI relativizeURI(URI sourceURI, URI targetURI, boolean msCompatible) { StringBuilder retVal = new StringBuilder(); String[] segmentsSource = sourceURI.getPath().split("/", -1); String[] segmentsTarget = targetURI.getPath().split("/", -1); // If the source URI is empty if (segmentsSource.length == 0) { throw new IllegalArgumentException("Can't relativize an empty source URI !"); } // If target URI is empty if (segmentsTarget.length == 0) { throw new IllegalArgumentException("Can't relativize an empty target URI !"); } // If the source is the root, then the relativized // form must actually be an absolute URI if (sourceURI.toString().equals("/")) { String path = targetURI.getPath(); if (msCompatible && path.length() > 0 && path.charAt(0) == '/') { try { targetURI = new URI(path.substring(1)); } catch (Exception e) { _logger.log(POILogger.WARN, e); return null; } } return targetURI; } // Relativize the source URI against the target URI. // First up, figure out how many steps along we can go // and still have them be the same int segmentsTheSame = 0; for (int i = 0; i < segmentsSource.length && i < segmentsTarget.length; i++) { if (segmentsSource[i].equals(segmentsTarget[i])) { // Match so far, good segmentsTheSame++; } else { break; } } // If we didn't have a good match or at least except a first empty element if ((segmentsTheSame == 0 || segmentsTheSame == 1) && segmentsSource[0].equals("") && segmentsTarget[0].equals("")) { for (int i = 0; i < segmentsSource.length - 2; i++) { retVal.append("../"); } for (int i = 0; i < segmentsTarget.length; i++) { if (segmentsTarget[i].equals("")) continue; retVal.append(segmentsTarget[i]); if (i != segmentsTarget.length - 1) retVal.append("/"); } try { return new URI(retVal.toString()); } catch (Exception e) { _logger.log(POILogger.WARN, e); return null; } } // Special case for where the two are the same if (segmentsTheSame == segmentsSource.length && segmentsTheSame == segmentsTarget.length) { if (sourceURI.equals(targetURI)) { // if source and target are the same they should be resolved to the last segment, // Example: if a slide references itself, e.g. the source URI is // "/ppt/slides/slide1.xml" and the targetURI is "slide1.xml" then // this it should be relativized as "slide1.xml", i.e. the last segment. retVal.append(segmentsSource[segmentsSource.length - 1]); } else { retVal.append(""); } } else { // Matched for so long, but no more // Do we need to go up a directory or two from // the source to get here? // (If it's all the way up, then don't bother!) if (segmentsTheSame == 1) { retVal.append("/"); } else { for (int j = segmentsTheSame; j < segmentsSource.length - 1; j++) { retVal.append("../"); } } // Now go from here on down for (int j = segmentsTheSame; j < segmentsTarget.length; j++) { if (retVal.length() > 0 && retVal.charAt(retVal.length() - 1) != '/') { retVal.append("/"); } retVal.append(segmentsTarget[j]); } } // if the target had a fragment then append it to the result String fragment = targetURI.getRawFragment(); if (fragment != null) { retVal.append("#").append(fragment); } try { return new URI(retVal.toString()); } catch (Exception e) { _logger.log(POILogger.WARN, e); return null; } } /** * Fully relativize the source part URI against the target part URI. * * @param sourceURI The source part URI. * @param targetURI The target part URI. * @return A fully relativize part name URI ('word/media/image1.gif', '/word/document.xml' => * 'media/image1.gif') else <code>null</code>. */ public static URI relativizeURI(URI sourceURI, URI targetURI) { return relativizeURI(sourceURI, targetURI, false); } /** * Resolve a source uri against a target. * * @param sourcePartUri The source URI. * @param targetUri The target URI. * @return The resolved URI. */ public static URI resolvePartUri(URI sourcePartUri, URI targetUri) { if (sourcePartUri == null || sourcePartUri.isAbsolute()) { throw new IllegalArgumentException("sourcePartUri invalid - " + sourcePartUri); } if (targetUri == null || targetUri.isAbsolute()) { throw new IllegalArgumentException("targetUri invalid - " + targetUri); } return sourcePartUri.resolve(targetUri); } /** Get URI from a string path. */ public static URI getURIFromPath(String path) { URI retUri; try { retUri = toURI(path); } catch (URISyntaxException e) { throw new IllegalArgumentException("path"); } return retUri; } /** * Get the source part URI from a specified relationships part. * * @param relationshipPartUri The relationship part use to retrieve the source part. * @return The source part URI from the specified relationships part. */ public static URI getSourcePartUriFromRelationshipPartUri(URI relationshipPartUri) { if (relationshipPartUri == null) throw new IllegalArgumentException("Must not be null"); if (!isRelationshipPartURI(relationshipPartUri)) throw new IllegalArgumentException("Must be a relationship part"); if (relationshipPartUri.compareTo(PACKAGE_RELATIONSHIPS_ROOT_URI) == 0) return PACKAGE_ROOT_URI; String filename = relationshipPartUri.getPath(); String filenameWithoutExtension = getFilenameWithoutExtension(relationshipPartUri); filename = filename.substring( 0, ((filename.length() - filenameWithoutExtension.length()) - RELATIONSHIP_PART_EXTENSION_NAME.length())); filename = filename.substring(0, filename.length() - RELATIONSHIP_PART_SEGMENT_NAME.length() - 1); filename = combine(filename, filenameWithoutExtension); return getURIFromPath(filename); } /** * Create an OPC compliant part name by throwing an exception if the URI is not valid. * * @param partUri The part name URI to validate. * @return A valid part name object, else <code>null</code>. * @throws InvalidFormatException Throws if the specified URI is not OPC compliant. */ public static PackagePartName createPartName(URI partUri) throws InvalidFormatException { if (partUri == null) throw new IllegalArgumentException("partName"); return new PackagePartName(partUri, true); } /** * Create an OPC compliant part name. * * @param partName The part name to validate. * @return The correspondant part name if valid, else <code>null</code>. * @throws InvalidFormatException Throws if the specified part name is not OPC compliant. * @see #createPartName(URI) */ public static PackagePartName createPartName(String partName) throws InvalidFormatException { URI partNameURI; try { partNameURI = toURI(partName); } catch (URISyntaxException e) { throw new InvalidFormatException(e.getMessage()); } return createPartName(partNameURI); } /** * Create an OPC compliant part name by resolving it using a base part. * * @param partName The part name to validate. * @param relativePart The relative base part. * @return The correspondant part name if valid, else <code>null</code>. * @throws InvalidFormatException Throws if the specified part name is not OPC compliant. * @see #createPartName(URI) */ public static PackagePartName createPartName(String partName, PackagePart relativePart) throws InvalidFormatException { URI newPartNameURI; try { newPartNameURI = resolvePartUri(relativePart.getPartName().getURI(), new URI(partName)); } catch (URISyntaxException e) { throw new InvalidFormatException(e.getMessage()); } return createPartName(newPartNameURI); } /** * Create an OPC compliant part name by resolving it using a base part. * * @param partName The part name URI to validate. * @param relativePart The relative base part. * @return The correspondant part name if valid, else <code>null</code>. * @throws InvalidFormatException Throws if the specified part name is not OPC compliant. * @see #createPartName(URI) */ public static PackagePartName createPartName(URI partName, PackagePart relativePart) throws InvalidFormatException { URI newPartNameURI = resolvePartUri(relativePart.getPartName().getURI(), partName); return createPartName(newPartNameURI); } /** * Validate a part URI by returning a boolean. ([M1.1],[M1.3],[M1.4],[M1.5],[M1.6]) * * <p>(OPC Specifications 8.1.1 Part names) : * * <p>Part Name Syntax * * <p>The part name grammar is defined as follows: * * <p><i>part_name = 1*( "/" segment ) * * <p>segment = 1*( pchar )</i> * * <p>(pchar is defined in RFC 3986) * * @param partUri The URI to validate. * @return <b>true</b> if the URI is valid to the OPC Specifications, else <b>false</b> * @see #createPartName(URI) */ public static boolean isValidPartName(URI partUri) { if (partUri == null) throw new IllegalArgumentException("partUri"); try { createPartName(partUri); return true; } catch (Exception e) { return false; } } /** * Decode a URI by converting all percent encoded character into a String character. * * @param uri The URI to decode. * @return The specified URI in a String with converted percent encoded characters. */ public static String decodeURI(URI uri) { StringBuffer retVal = new StringBuffer(); String uriStr = uri.toASCIIString(); char c; for (int i = 0; i < uriStr.length(); ++i) { c = uriStr.charAt(i); if (c == '%') { // We certainly found an encoded character, check for length // now ( '%' HEXDIGIT HEXDIGIT) if (((uriStr.length() - i) < 2)) { throw new IllegalArgumentException( "The uri " + uriStr + " contain invalid encoded character !"); } // Decode the encoded character char decodedChar = (char) Integer.parseInt(uriStr.substring(i + 1, i + 3), 16); retVal.append(decodedChar); i += 2; continue; } retVal.append(c); } return retVal.toString(); } /** * Build a part name where the relationship should be stored ((ex /word/document.xml -> * /word/_rels/document.xml.rels) * * @param partName Source part URI * @return the full path (as URI) of the relation file * @throws InvalidOperationException Throws if the specified URI is a relationshp part. */ public static PackagePartName getRelationshipPartName(PackagePartName partName) { if (partName == null) throw new IllegalArgumentException("partName"); if (PackagingURIHelper.PACKAGE_ROOT_URI.getPath().equals(partName.getURI().getPath())) return PackagingURIHelper.PACKAGE_RELATIONSHIPS_ROOT_PART_NAME; if (partName.isRelationshipPartURI()) throw new InvalidOperationException("Can't be a relationship part"); String fullPath = partName.getURI().getPath(); String filename = getFilename(partName.getURI()); fullPath = fullPath.substring(0, fullPath.length() - filename.length()); fullPath = combine(fullPath, PackagingURIHelper.RELATIONSHIP_PART_SEGMENT_NAME); fullPath = combine(fullPath, filename); fullPath = fullPath + PackagingURIHelper.RELATIONSHIP_PART_EXTENSION_NAME; PackagePartName retPartName; try { retPartName = createPartName(fullPath); } catch (InvalidFormatException e) { // Should never happen in production as all data are fixed but in // case of return null: return null; } return retPartName; } /** * Convert a string to {@link java.net.URI} * * <p>If part name is not a valid URI, it is resolved as follows: * * <p>1. Percent-encode each open bracket ([) and close bracket (]). 2. Percent-encode each * percent (%) character that is not followed by a hexadecimal notation of an octet value. 3. * Un-percent-encode each percent-encoded unreserved character. 4. Un-percent-encode each forward * slash (/) and back slash (\). 5. Convert all back slashes to forward slashes. 6. If present in * a segment containing non-dot (?.?) characters, remove trailing dot (?.?) characters from each * segment. 7. Replace each occurrence of multiple consecutive forward slashes (/) with a single * forward slash. 8. If a single trailing forward slash (/) is present, remove that trailing * forward slash. 9. Remove complete segments that consist of three or more dots. 10. Resolve the * relative reference against the base URI of the part holding the Unicode string, as it is * defined in ?5.2 of RFC 3986. The path component of the resulting absolute URI is the part name. * * @param value the string to be parsed into a URI * @return the resolved part name that should be OK to construct a URI * <p>TODO YK: for now this method does only (5). Finish the rest. */ public static URI toURI(String value) throws URISyntaxException { // 5. Convert all back slashes to forward slashes if (value.indexOf("\\") != -1) { value = value.replace('\\', '/'); } // URI fragemnts (those starting with '#') are not encoded // and may contain white spaces and raw unicode characters int fragmentIdx = value.indexOf('#'); if (fragmentIdx != -1) { String path = value.substring(0, fragmentIdx); String fragment = value.substring(fragmentIdx + 1); value = path + "#" + encode(fragment); } return new URI(value); } /** * percent-encode white spaces and characters above 0x80. * * <p>Examples: 'Apache POI' --> 'Apache%20POI' 'Apache\u0410POI' --> 'Apache%04%10POI' * * @param s the string to encode * @return the encoded string */ public static String encode(String s) { int n = s.length(); if (n == 0) return s; ByteBuffer bb; try { bb = ByteBuffer.wrap(s.getBytes("UTF-8")); } catch (UnsupportedEncodingException e) { // should not happen throw new RuntimeException(e); } StringBuilder sb = new StringBuilder(); while (bb.hasRemaining()) { int b = bb.get() & 0xff; if (isUnsafe(b)) { sb.append('%'); sb.append(hexDigits[(b >> 4) & 0x0F]); sb.append(hexDigits[(b >> 0) & 0x0F]); } else { sb.append((char) b); } } return sb.toString(); } private static final char[] hexDigits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; private static boolean isUnsafe(int ch) { return ch > 0x80 || " ".indexOf(ch) >= 0; } }
/** * High level representation of a ooxml slideshow. This is the first object most users will * construct whether they are reading or writing a slideshow. It is also the top level object for * creating new slides/etc. */ @Beta public class XMLSlideShow extends POIXMLDocument { private static POILogger _logger = POILogFactory.getLogger(XMLSlideShow.class); private CTPresentation _presentation; private List<XSLFSlide> _slides; private Map<String, XSLFSlideMaster> _masters; private List<XSLFPictureData> _pictures; private XSLFTableStyles _tableStyles; private XSLFNotesMaster _notesMaster; private XSLFCommentAuthors _commentAuthors; public XMLSlideShow() { this(empty()); } public XMLSlideShow(OPCPackage pkg) { super(pkg); try { if (getCorePart().getContentType().equals(XSLFRelation.THEME_MANAGER.getContentType())) { rebase(getPackage()); } // build a tree of POIXMLDocumentParts, this presentation being the root load(XSLFFactory.getInstance()); } catch (Exception e) { throw new POIXMLException(e); } } public XMLSlideShow(InputStream is) throws IOException { this(PackageHelper.open(is)); } static final OPCPackage empty() { InputStream is = XMLSlideShow.class.getResourceAsStream("empty.pptx"); if (is == null) { throw new RuntimeException("Missing resource 'empty.pptx'"); } try { return OPCPackage.open(is); } catch (Exception e) { throw new POIXMLException(e); } } // TODO get rid of this method @Deprecated public XSLFSlideShow _getXSLFSlideShow() throws OpenXML4JException, IOException, XmlException { return new XSLFSlideShow(getPackage()); } @Override @SuppressWarnings("deprecation") protected void onDocumentRead() throws IOException { try { PresentationDocument doc = PresentationDocument.Factory.parse(getCorePart().getInputStream()); _presentation = doc.getPresentation(); Map<String, XSLFSlide> shIdMap = new HashMap<String, XSLFSlide>(); _masters = new HashMap<String, XSLFSlideMaster>(); for (POIXMLDocumentPart p : getRelations()) { if (p instanceof XSLFSlide) { shIdMap.put(p.getPackageRelationship().getId(), (XSLFSlide) p); } else if (p instanceof XSLFSlideMaster) { XSLFSlideMaster master = (XSLFSlideMaster) p; _masters.put(p.getPackageRelationship().getId(), master); } else if (p instanceof XSLFTableStyles) { _tableStyles = (XSLFTableStyles) p; } else if (p instanceof XSLFNotesMaster) { _notesMaster = (XSLFNotesMaster) p; } else if (p instanceof XSLFCommentAuthors) { _commentAuthors = (XSLFCommentAuthors) p; } } _slides = new ArrayList<XSLFSlide>(); if (_presentation.isSetSldIdLst()) { for (CTSlideIdListEntry slId : _presentation.getSldIdLst().getSldIdArray()) { XSLFSlide sh = shIdMap.get(slId.getId2()); if (sh == null) { _logger.log( POILogger.WARN, "Slide with r:id " + slId.getId() + " was defined, but didn't exist in package, skipping"); continue; } _slides.add(sh); } } } catch (XmlException e) { throw new POIXMLException(e); } } @Override protected void commit() throws IOException { XmlOptions xmlOptions = new XmlOptions(DEFAULT_XML_OPTIONS); Map<String, String> map = new HashMap<String, String>(); map.put(STRelationshipId.type.getName().getNamespaceURI(), "r"); xmlOptions.setSaveSuggestedPrefixes(map); PackagePart part = getPackagePart(); OutputStream out = part.getOutputStream(); _presentation.save(out, xmlOptions); out.close(); } /** Get the document's embedded files. */ @Override public List<PackagePart> getAllEmbedds() throws OpenXML4JException { return Collections.unmodifiableList( getPackage().getPartsByName(Pattern.compile("/ppt/embeddings/.*?"))); } /** * Returns all Pictures, which are referenced from the document itself. * * @return a {@link List} of {@link PackagePart}. The returned {@link List} is unmodifiable. */ public List<XSLFPictureData> getAllPictures() { if (_pictures == null) { List<PackagePart> mediaParts = getPackage().getPartsByName(Pattern.compile("/ppt/media/.*?")); _pictures = new ArrayList<XSLFPictureData>(mediaParts.size()); for (PackagePart part : mediaParts) { _pictures.add(new XSLFPictureData(part, null)); } } return Collections.unmodifiableList(_pictures); } /** * Create a slide and initialize it from the specified layout. * * @param layout * @return created slide */ @SuppressWarnings("deprecation") public XSLFSlide createSlide(XSLFSlideLayout layout) { int slideNumber = 256, cnt = 1; CTSlideIdList slideList; if (!_presentation.isSetSldIdLst()) slideList = _presentation.addNewSldIdLst(); else { slideList = _presentation.getSldIdLst(); for (CTSlideIdListEntry slideId : slideList.getSldIdArray()) { slideNumber = (int) Math.max(slideId.getId() + 1, slideNumber); cnt++; } } XSLFSlide slide = (XSLFSlide) createRelationship(XSLFRelation.SLIDE, XSLFFactory.getInstance(), cnt); CTSlideIdListEntry slideId = slideList.addNewSldId(); slideId.setId(slideNumber); slideId.setId2(slide.getPackageRelationship().getId()); layout.copyLayout(slide); slide.addRelation(layout.getPackageRelationship().getId(), layout); PackagePartName ppName = layout.getPackagePart().getPartName(); slide .getPackagePart() .addRelationship( ppName, TargetMode.INTERNAL, layout.getPackageRelationship().getRelationshipType()); _slides.add(slide); return slide; } /** Create a blank slide. */ public XSLFSlide createSlide() { String masterId = _presentation.getSldMasterIdLst().getSldMasterIdArray(0).getId2(); XSLFSlideMaster master = _masters.get(masterId); XSLFSlideLayout layout = master.getLayout(SlideLayout.BLANK); if (layout == null) throw new IllegalArgumentException("Blank layout was not found"); return createSlide(layout); } /** Return notes slide for the specified slide or create new if it does not exist yet. */ public XSLFNotes getNotesSlide(XSLFSlide slide) { XSLFNotes notesSlide = slide.getNotes(); if (notesSlide == null) { notesSlide = createNotesSlide(slide); } return notesSlide; } /** Create a blank notes slide. */ private XSLFNotes createNotesSlide(XSLFSlide slide) { if (_notesMaster == null) { createNotesMaster(); } Integer slideIndex = XSLFRelation.SLIDE.getFileNameIndex(slide); XSLFNotes notesSlide = (XSLFNotes) createRelationship(XSLFRelation.NOTES, XSLFFactory.getInstance(), slideIndex); notesSlide.addRelation(_notesMaster.getPackageRelationship().getId(), _notesMaster); PackagePartName notesMasterPackagePartName = _notesMaster.getPackagePart().getPartName(); notesSlide .getPackagePart() .addRelationship( notesMasterPackagePartName, TargetMode.INTERNAL, _notesMaster.getPackageRelationship().getRelationshipType()); slide.addRelation(notesSlide.getPackageRelationship().getId(), notesSlide); PackagePartName notesSlidesPackagePartName = notesSlide.getPackagePart().getPartName(); slide .getPackagePart() .addRelationship( notesSlidesPackagePartName, TargetMode.INTERNAL, notesSlide.getPackageRelationship().getRelationshipType()); notesSlide.addRelation(slide.getPackageRelationship().getId(), slide); PackagePartName slidesPackagePartName = slide.getPackagePart().getPartName(); notesSlide .getPackagePart() .addRelationship( slidesPackagePartName, TargetMode.INTERNAL, slide.getPackageRelationship().getRelationshipType()); notesSlide.importContent(_notesMaster); return notesSlide; } /** Create a notes master. */ public void createNotesMaster() { _notesMaster = (XSLFNotesMaster) createRelationship(XSLFRelation.NOTES_MASTER, XSLFFactory.getInstance(), 1); CTNotesMasterIdList notesMasterIdList = _presentation.addNewNotesMasterIdLst(); CTNotesMasterIdListEntry notesMasterId = notesMasterIdList.addNewNotesMasterId(); notesMasterId.setId(_notesMaster.getPackageRelationship().getId()); Integer themeIndex = 1; List<Integer> themeIndexList = new ArrayList<Integer>(); for (POIXMLDocumentPart p : getRelations()) { if (p instanceof XSLFTheme) { themeIndexList.add(XSLFRelation.THEME.getFileNameIndex(p)); } } if (!themeIndexList.isEmpty()) { Boolean found = false; for (Integer i = 1; i <= themeIndexList.size(); i++) { if (!themeIndexList.contains(i)) { found = true; themeIndex = i; } } if (!found) { themeIndex = themeIndexList.size() + 1; } } XSLFTheme theme = (XSLFTheme) createRelationship(XSLFRelation.THEME, XSLFFactory.getInstance(), themeIndex); theme.importTheme(getSlides()[0].getTheme()); _notesMaster.addRelation(theme.getPackageRelationship().getId(), theme); PackagePartName themePackagePartName = theme.getPackagePart().getPartName(); _notesMaster .getPackagePart() .addRelationship( themePackagePartName, TargetMode.INTERNAL, theme.getPackageRelationship().getRelationshipType()); } /** Return the Notes Master, if there is one. (May not be present if no notes exist) */ public XSLFNotesMaster getNotesMaster() { return _notesMaster; } public XSLFSlideMaster[] getSlideMasters() { return _masters.values().toArray(new XSLFSlideMaster[_masters.size()]); } /** Return all the slides in the slideshow */ public XSLFSlide[] getSlides() { return _slides.toArray(new XSLFSlide[_slides.size()]); } /** * Returns the list of comment authors, if there is one. Will only be present if at least one * slide has comments on it. */ public XSLFCommentAuthors getCommentAuthors() { return _commentAuthors; } /** @param newIndex 0-based index of the slide */ @SuppressWarnings("deprecation") public void setSlideOrder(XSLFSlide slide, int newIndex) { int oldIndex = _slides.indexOf(slide); if (oldIndex == -1) throw new IllegalArgumentException("Slide not found"); if (oldIndex == newIndex) return; // fix the usermodel container _slides.add(newIndex, _slides.remove(oldIndex)); // fix ordering in the low-level xml CTSlideIdList sldIdLst = _presentation.getSldIdLst(); CTSlideIdListEntry[] entries = sldIdLst.getSldIdArray(); CTSlideIdListEntry oldEntry = entries[oldIndex]; if (oldIndex < newIndex) { System.arraycopy(entries, oldIndex + 1, entries, oldIndex, newIndex - oldIndex); } else { System.arraycopy(entries, newIndex, entries, newIndex + 1, oldIndex - newIndex); } entries[newIndex] = oldEntry; sldIdLst.setSldIdArray(entries); } public XSLFSlide removeSlide(int index) { XSLFSlide slide = _slides.remove(index); removeRelation(slide); _presentation.getSldIdLst().removeSldId(index); return slide; } /** * Returns the current page size * * @return the page size */ public Dimension getPageSize() { CTSlideSize sz = _presentation.getSldSz(); int cx = sz.getCx(); int cy = sz.getCy(); return new Dimension((int) Units.toPoints(cx), (int) Units.toPoints(cy)); } /** * Sets the page size to the given <code>Dimension</code> object. * * @param pgSize page size */ public void setPageSize(Dimension pgSize) { CTSlideSize sz = CTSlideSize.Factory.newInstance(); sz.setCx(Units.toEMU(pgSize.getWidth())); sz.setCy(Units.toEMU(pgSize.getHeight())); _presentation.setSldSz(sz); } @Internal public CTPresentation getCTPresentation() { return _presentation; } /** * Adds a picture to the workbook. * * @param pictureData The bytes of the picture * @param format The format of the picture. * @return the index to this picture (1 based). * @see XSLFPictureData#PICTURE_TYPE_EMF * @see XSLFPictureData#PICTURE_TYPE_WMF * @see XSLFPictureData#PICTURE_TYPE_PICT * @see XSLFPictureData#PICTURE_TYPE_JPEG * @see XSLFPictureData#PICTURE_TYPE_PNG * @see XSLFPictureData#PICTURE_TYPE_DIB */ public int addPicture(byte[] pictureData, int format) { XSLFPictureData img = findPictureData(pictureData); POIXMLRelation relDesc = XSLFPictureData.RELATIONS[format]; if (img == null) { int imageNumber = _pictures.size(); img = (XSLFPictureData) createRelationship( XSLFPictureData.RELATIONS[format], XSLFFactory.getInstance(), imageNumber + 1, true); _pictures.add(img); try { OutputStream out = img.getPackagePart().getOutputStream(); out.write(pictureData); out.close(); } catch (IOException e) { throw new POIXMLException(e); } return _pictures.size() - 1; } else { return _pictures.indexOf(img); } } /** check if a picture with this picture data already exists in this presentation */ XSLFPictureData findPictureData(byte[] pictureData) { long checksum = IOUtils.calculateChecksum(pictureData); for (XSLFPictureData pic : getAllPictures()) { if (pic.getChecksum() == checksum) { return pic; } } return null; } public XSLFTableStyles getTableStyles() { return _tableStyles; } CTTextParagraphProperties getDefaultParagraphStyle(int level) { XmlObject[] o = _presentation.selectPath( "declare namespace p='http://schemas.openxmlformats.org/presentationml/2006/main' " + "declare namespace a='http://schemas.openxmlformats.org/drawingml/2006/main' " + ".//p:defaultTextStyle/a:lvl" + (level + 1) + "pPr"); if (o.length == 1) { return (CTTextParagraphProperties) o[0]; } return null; } }
public final class FIBFieldHandler { public static final int STSHFORIG = 0; public static final int STSHF = 1; public static final int PLCFFNDREF = 2; public static final int PLCFFNDTXT = 3; public static final int PLCFANDREF = 4; public static final int PLCFANDTXT = 5; public static final int PLCFSED = 6; public static final int PLCFPAD = 7; public static final int PLCFPHE = 8; public static final int STTBGLSY = 9; public static final int PLCFGLSY = 10; public static final int PLCFHDD = 11; public static final int PLCFBTECHPX = 12; public static final int PLCFBTEPAPX = 13; public static final int PLCFSEA = 14; public static final int STTBFFFN = 15; public static final int PLCFFLDMOM = 16; public static final int PLCFFLDHDR = 17; public static final int PLCFFLDFTN = 18; public static final int PLCFFLDATN = 19; public static final int PLCFFLDMCR = 20; public static final int STTBFBKMK = 21; public static final int PLCFBKF = 22; public static final int PLCFBKL = 23; public static final int CMDS = 24; public static final int PLCMCR = 25; public static final int STTBFMCR = 26; public static final int PRDRVR = 27; public static final int PRENVPORT = 28; public static final int PRENVLAND = 29; public static final int WSS = 30; public static final int DOP = 31; public static final int STTBFASSOC = 32; public static final int CLX = 33; public static final int PLCFPGDFTN = 34; public static final int AUTOSAVESOURCE = 35; public static final int GRPXSTATNOWNERS = 36; public static final int STTBFATNBKMK = 37; public static final int PLCFDOAMOM = 38; public static final int PLCDOAHDR = 39; public static final int PLCSPAMOM = 40; public static final int PLCSPAHDR = 41; public static final int PLCFATNBKF = 42; public static final int PLCFATNBKL = 43; public static final int PMS = 44; public static final int FORMFLDSTTBS = 45; public static final int PLCFENDREF = 46; public static final int PLCFENDTXT = 47; public static final int PLCFFLDEDN = 48; public static final int PLCFPGDEDN = 49; public static final int DGGINFO = 50; public static final int STTBFRMARK = 51; public static final int STTBCAPTION = 52; public static final int STTBAUTOCAPTION = 53; public static final int PLCFWKB = 54; public static final int PLCFSPL = 55; public static final int PLCFTXBXTXT = 56; public static final int PLCFFLDTXBX = 57; public static final int PLCFHDRTXBXTXT = 58; public static final int PLCFFLDHDRTXBX = 59; public static final int STWUSER = 60; public static final int STTBTTMBD = 61; public static final int UNUSED = 62; public static final int PGDMOTHER = 63; public static final int BKDMOTHER = 64; public static final int PGDFTN = 65; public static final int BKDFTN = 66; public static final int PGDEDN = 67; public static final int BKDEDN = 68; public static final int STTBFINTFLD = 69; public static final int ROUTESLIP = 70; public static final int STTBSAVEDBY = 71; public static final int STTBFNM = 72; public static final int PLCFLST = 73; public static final int PLFLFO = 74; public static final int PLCFTXBXBKD = 75; public static final int PLCFTXBXHDRBKD = 76; public static final int DOCUNDO = 77; public static final int RGBUSE = 78; public static final int USP = 79; public static final int USKF = 80; public static final int PLCUPCRGBUSE = 81; public static final int PLCUPCUSP = 82; public static final int STTBGLSYSTYLE = 83; public static final int PLGOSL = 84; public static final int PLCOCX = 85; public static final int PLCFBTELVC = 86; public static final int MODIFIED = 87; public static final int PLCFLVC = 88; public static final int PLCASUMY = 89; public static final int PLCFGRAM = 90; public static final int STTBLISTNAMES = 91; public static final int STTBFUSSR = 92; private static POILogger log = POILogFactory.getLogger(org / apache / poi / hwpf / model / FIBFieldHandler); private static final int FIELD_SIZE = 8; private Map _unknownMap; private int _fields[]; public FIBFieldHandler( byte mainStream[], int startOffset, byte tableStream[], HashSet offsetList, boolean areKnown) { _unknownMap = new HashMap(); int numFields = LittleEndian.getShort(mainStream, startOffset); int offset = startOffset + 2; _fields = new int[numFields * 2]; for (int x = 0; x < numFields; x++) { int fieldOffset = x * 8 + offset; int dsOffset = LittleEndian.getInt(mainStream, fieldOffset); fieldOffset += 4; int dsSize = LittleEndian.getInt(mainStream, fieldOffset); if (offsetList.contains(Integer.valueOf(x)) ^ areKnown && dsSize > 0) if (dsOffset + dsSize > tableStream.length) { log.log( POILogger.WARN, (new StringBuilder()) .append("Unhandled data structure points to outside the buffer. offset = ") .append(dsOffset) .append(", length = ") .append(dsSize) .append(", buffer length = ") .append(tableStream.length) .toString()); } else { UnhandledDataStructure unhandled = new UnhandledDataStructure(tableStream, dsOffset, dsSize); _unknownMap.put(Integer.valueOf(x), unhandled); } _fields[x * 2] = dsOffset; _fields[x * 2 + 1] = dsSize; } } public void clearFields() { Arrays.fill(_fields, 0); } public int getFieldOffset(int field) { return _fields[field * 2]; } public int getFieldSize(int field) { return _fields[field * 2 + 1]; } public void setFieldOffset(int field, int offset) { _fields[field * 2] = offset; } public void setFieldSize(int field, int size) { _fields[field * 2 + 1] = size; } public int sizeInBytes() { return _fields.length * 4 + 2; } void writeTo(byte mainStream[], int offset, HWPFOutputStream tableStream) throws IOException { int length = _fields.length / 2; LittleEndian.putShort(mainStream, offset, (short) length); offset += 2; for (int x = 0; x < length; x++) { UnhandledDataStructure ds = (UnhandledDataStructure) _unknownMap.get(Integer.valueOf(x)); if (ds != null) { _fields[x * 2] = tableStream.getOffset(); LittleEndian.putInt(mainStream, offset, tableStream.getOffset()); offset += 4; byte buf[] = ds.getBuf(); tableStream.write(buf); _fields[x * 2 + 1] = buf.length; LittleEndian.putInt(mainStream, offset, buf.length); offset += 4; } else { LittleEndian.putInt(mainStream, offset, _fields[x * 2]); offset += 4; LittleEndian.putInt(mainStream, offset, _fields[x * 2 + 1]); offset += 4; } } } }
/** * The <code>HyperlinkRecord</code> (0x01B8) wraps an HLINK-record from the Excel-97 format. * Supports only external links for now (eg http://) * * @author Mark Hissink Muller <a href="mailto:[email protected] >mark&064;hissinkmuller.nl</a> * @author Yegor Kozlov (yegor at apache dot org) */ public final class HyperlinkRecord extends StandardRecord { public static final short sid = 0x01B8; private POILogger logger = POILogFactory.getLogger(getClass()); static final class GUID { /* * this class is currently only used here, but could be moved to a * common package if needed */ private static final int TEXT_FORMAT_LENGTH = 36; public static final int ENCODED_SIZE = 16; /** 4 bytes - little endian */ private final int _d1; /** 2 bytes - little endian */ private final int _d2; /** 2 bytes - little endian */ private final int _d3; /** 8 bytes - serialized as big endian, stored with inverted endianness here */ private final long _d4; public GUID(LittleEndianInput in) { this(in.readInt(), in.readUShort(), in.readUShort(), in.readLong()); } public GUID(int d1, int d2, int d3, long d4) { _d1 = d1; _d2 = d2; _d3 = d3; _d4 = d4; } public void serialize(LittleEndianOutput out) { out.writeInt(_d1); out.writeShort(_d2); out.writeShort(_d3); out.writeLong(_d4); } @Override public boolean equals(Object obj) { GUID other = (GUID) obj; if (obj == null || !(obj instanceof GUID)) return false; return _d1 == other._d1 && _d2 == other._d2 && _d3 == other._d3 && _d4 == other._d4; } public int getD1() { return _d1; } public int getD2() { return _d2; } public int getD3() { return _d3; } public long getD4() { // ByteArrayOutputStream baos = new ByteArrayOutputStream(8); try { new DataOutputStream(baos).writeLong(_d4); } catch (IOException e) { throw new RuntimeException(e); } byte[] buf = baos.toByteArray(); return new LittleEndianByteArrayInputStream(buf).readLong(); } public String formatAsString() { StringBuilder sb = new StringBuilder(36); int PREFIX_LEN = "0x".length(); sb.append(HexDump.intToHex(_d1), PREFIX_LEN, 8); sb.append("-"); sb.append(HexDump.shortToHex(_d2), PREFIX_LEN, 4); sb.append("-"); sb.append(HexDump.shortToHex(_d3), PREFIX_LEN, 4); sb.append("-"); char[] d4Chars = HexDump.longToHex(getD4()); sb.append(d4Chars, PREFIX_LEN, 4); sb.append("-"); sb.append(d4Chars, PREFIX_LEN + 4, 12); return sb.toString(); } @Override public String toString() { StringBuilder sb = new StringBuilder(64); sb.append(getClass().getName()).append(" ["); sb.append(formatAsString()); sb.append("]"); return sb.toString(); } /** * Read a GUID in standard text form e.g.<br> * 13579BDF-0246-8ACE-0123-456789ABCDEF <br> * -> <br> * 0x13579BDF, 0x0246, 0x8ACE 0x0123456789ABCDEF */ public static GUID parse(String rep) { char[] cc = rep.toCharArray(); if (cc.length != TEXT_FORMAT_LENGTH) { throw new RecordFormatException("supplied text is the wrong length for a GUID"); } int d0 = (parseShort(cc, 0) << 16) + (parseShort(cc, 4) << 0); int d1 = parseShort(cc, 9); int d2 = parseShort(cc, 14); for (int i = 23; i > 19; i--) { cc[i] = cc[i - 1]; } long d3 = parseLELong(cc, 20); return new GUID(d0, d1, d2, d3); } private static long parseLELong(char[] cc, int startIndex) { long acc = 0; for (int i = startIndex + 14; i >= startIndex; i -= 2) { acc <<= 4; acc += parseHexChar(cc[i + 0]); acc <<= 4; acc += parseHexChar(cc[i + 1]); } return acc; } private static int parseShort(char[] cc, int startIndex) { int acc = 0; for (int i = 0; i < 4; i++) { acc <<= 4; acc += parseHexChar(cc[startIndex + i]); } return acc; } private static int parseHexChar(char c) { if (c >= '0' && c <= '9') { return c - '0'; } if (c >= 'A' && c <= 'F') { return c - 'A' + 10; } if (c >= 'a' && c <= 'f') { return c - 'a' + 10; } throw new RecordFormatException("Bad hex char '" + c + "'"); } } /** Link flags */ static final int HLINK_URL = 0x01; // File link or URL. static final int HLINK_ABS = 0x02; // Absolute path. static final int HLINK_LABEL = 0x14; // Has label/description. /** Place in worksheet. If set, the {@link #_textMark} field will be present */ static final int HLINK_PLACE = 0x08; private static final int HLINK_TARGET_FRAME = 0x80; // has 'target frame' private static final int HLINK_UNC_PATH = 0x100; // has UNC path static final GUID STD_MONIKER = GUID.parse("79EAC9D0-BAF9-11CE-8C82-00AA004BA90B"); static final GUID URL_MONIKER = GUID.parse("79EAC9E0-BAF9-11CE-8C82-00AA004BA90B"); static final GUID FILE_MONIKER = GUID.parse("00000303-0000-0000-C000-000000000046"); /** expected Tail of a URL link */ private static final byte[] URL_TAIL = HexRead.readFromString( "79 58 81 F4 3B 1D 7F 48 AF 2C 82 5D C4 85 27 63 00 00 00 00 A5 AB 00 00"); /** expected Tail of a file link */ private static final byte[] FILE_TAIL = HexRead.readFromString( "FF FF AD DE 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00"); private static final int TAIL_SIZE = FILE_TAIL.length; /** cell range of this hyperlink */ private CellRangeAddress _range; /** 16-byte GUID */ private GUID _guid; /** Some sort of options for file links. */ private int _fileOpts; /** Link options. Can include any of HLINK_* flags. */ private int _linkOpts; /** Test label */ private String _label; private String _targetFrame; /** Moniker. Makes sense only for URL and file links */ private GUID _moniker; /** in 8:3 DOS format No Unicode string header, always 8-bit characters, zero-terminated */ private String _shortFilename; /** Link */ private String _address; /** * Text describing a place in document. In Excel UI, this is appended to the address, (after a '#' * delimiter).<br> * This field is optional. If present, the {@link #HLINK_PLACE} must be set. */ private String _textMark; private byte[] _uninterpretedTail; /** Create a new hyperlink */ public HyperlinkRecord() {} /** @return the 0-based column of the first cell that contains this hyperlink */ public int getFirstColumn() { return _range.getFirstColumn(); } /** Set the first column (zero-based)of the range that contains this hyperlink */ public void setFirstColumn(int col) { _range.setFirstColumn(col); } /** @return the 0-based column of the last cell that contains this hyperlink */ public int getLastColumn() { return _range.getLastColumn(); } /** Set the last column (zero-based)of the range that contains this hyperlink */ public void setLastColumn(int col) { _range.setLastColumn(col); } /** @return the 0-based row of the first cell that contains this hyperlink */ public int getFirstRow() { return _range.getFirstRow(); } /** Set the first row (zero-based)of the range that contains this hyperlink */ public void setFirstRow(int col) { _range.setFirstRow(col); } /** @return the 0-based row of the last cell that contains this hyperlink */ public int getLastRow() { return _range.getLastRow(); } /** Set the last row (zero-based)of the range that contains this hyperlink */ public void setLastRow(int col) { _range.setLastRow(col); } /** @return 16-byte guid identifier Seems to always equal {@link #STD_MONIKER} */ GUID getGuid() { return _guid; } /** @return 16-byte moniker */ GUID getMoniker() { return _moniker; } private static String cleanString(String s) { if (s == null) { return null; } int idx = s.indexOf('\u0000'); if (idx < 0) { return s; } return s.substring(0, idx); } private static String appendNullTerm(String s) { if (s == null) { return null; } return s + '\u0000'; } /** * Return text label for this hyperlink * * @return text to display */ public String getLabel() { return cleanString(_label); } /** * Sets text label for this hyperlink * * @param label text label for this hyperlink */ public void setLabel(String label) { _label = appendNullTerm(label); } public String getTargetFrame() { return cleanString(_targetFrame); } /** * Hyperlink address. Depending on the hyperlink type it can be URL, e-mail, path to a file, etc. * * @return the address of this hyperlink */ public String getAddress() { if ((_linkOpts & HLINK_URL) != 0 && FILE_MONIKER.equals(_moniker)) return cleanString(_address != null ? _address : _shortFilename); else if ((_linkOpts & HLINK_PLACE) != 0) return cleanString(_textMark); else return cleanString(_address); } /** * Hyperlink address. Depending on the hyperlink type it can be URL, e-mail, path to a file, etc. * * @param address the address of this hyperlink */ public void setAddress(String address) { if ((_linkOpts & HLINK_URL) != 0 && FILE_MONIKER.equals(_moniker)) _shortFilename = appendNullTerm(address); else if ((_linkOpts & HLINK_PLACE) != 0) _textMark = appendNullTerm(address); else _address = appendNullTerm(address); } public String getShortFilename() { return cleanString(_shortFilename); } public void setShortFilename(String shortFilename) { _shortFilename = appendNullTerm(shortFilename); } public String getTextMark() { return cleanString(_textMark); } public void setTextMark(String textMark) { _textMark = appendNullTerm(textMark); } /** Link options. Must be a combination of HLINK_* constants. For testing only */ int getLinkOptions() { return _linkOpts; } /** Label options */ public int getLabelOptions() { return 2; // always 2 } /** Options for a file link */ public int getFileOptions() { return _fileOpts; } public HyperlinkRecord(RecordInputStream in) { _range = new CellRangeAddress(in); _guid = new GUID(in); /** * streamVersion (4 bytes): An unsigned integer that specifies the version number of the * serialization implementation used to save this structure. This value MUST equal 2. */ int streamVersion = in.readInt(); if (streamVersion != 0x00000002) { throw new RecordFormatException("Stream Version must be 0x2 but found " + streamVersion); } _linkOpts = in.readInt(); if ((_linkOpts & HLINK_LABEL) != 0) { int label_len = in.readInt(); _label = in.readUnicodeLEString(label_len); } if ((_linkOpts & HLINK_TARGET_FRAME) != 0) { int len = in.readInt(); _targetFrame = in.readUnicodeLEString(len); } if ((_linkOpts & HLINK_URL) != 0 && (_linkOpts & HLINK_UNC_PATH) != 0) { _moniker = null; int nChars = in.readInt(); _address = in.readUnicodeLEString(nChars); } if ((_linkOpts & HLINK_URL) != 0 && (_linkOpts & HLINK_UNC_PATH) == 0) { _moniker = new GUID(in); if (URL_MONIKER.equals(_moniker)) { int length = in.readInt(); /** * The value of <code>length<code> be either the byte size of the url field * (including the terminating NULL character) or the byte size of the url field plus 24. * If the value of this field is set to the byte size of the url field, * then the tail bytes fields are not present. */ int remaining = in.remaining(); if (length == remaining) { int nChars = length / 2; _address = in.readUnicodeLEString(nChars); } else { int nChars = (length - TAIL_SIZE) / 2; _address = in.readUnicodeLEString(nChars); /** * TODO: make sense of the remaining bytes According to the spec they consist of: 1. * 16-byte GUID: This field MUST equal {0xF4815879, 0x1D3B, 0x487F, 0xAF, 0x2C, 0x82, * 0x5D, 0xC4, 0x85, 0x27, 0x63} 2. Serial version, this field MUST equal 0 if present. 3. * URI Flags */ _uninterpretedTail = readTail(URL_TAIL, in); } } else if (FILE_MONIKER.equals(_moniker)) { _fileOpts = in.readShort(); int len = in.readInt(); _shortFilename = StringUtil.readCompressedUnicode(in, len); _uninterpretedTail = readTail(FILE_TAIL, in); int size = in.readInt(); if (size > 0) { int charDataSize = in.readInt(); // From the spec: An optional unsigned integer that MUST be 3 if present // but some files has 4 int usKeyValue = in.readUShort(); _address = StringUtil.readUnicodeLE(in, charDataSize / 2); } else { _address = null; } } else if (STD_MONIKER.equals(_moniker)) { _fileOpts = in.readShort(); int len = in.readInt(); byte[] path_bytes = new byte[len]; in.readFully(path_bytes); _address = new String(path_bytes); } } if ((_linkOpts & HLINK_PLACE) != 0) { int len = in.readInt(); _textMark = in.readUnicodeLEString(len); } if (in.remaining() > 0) { logger.log( POILogger.WARN, "Hyperlink data remains: " + in.remaining() + " : " + HexDump.toHex(in.readRemainder())); } } public void serialize(LittleEndianOutput out) { _range.serialize(out); _guid.serialize(out); out.writeInt(0x00000002); // TODO const out.writeInt(_linkOpts); if ((_linkOpts & HLINK_LABEL) != 0) { out.writeInt(_label.length()); StringUtil.putUnicodeLE(_label, out); } if ((_linkOpts & HLINK_TARGET_FRAME) != 0) { out.writeInt(_targetFrame.length()); StringUtil.putUnicodeLE(_targetFrame, out); } if ((_linkOpts & HLINK_URL) != 0 && (_linkOpts & HLINK_UNC_PATH) != 0) { out.writeInt(_address.length()); StringUtil.putUnicodeLE(_address, out); } if ((_linkOpts & HLINK_URL) != 0 && (_linkOpts & HLINK_UNC_PATH) == 0) { _moniker.serialize(out); if (URL_MONIKER.equals(_moniker)) { if (_uninterpretedTail == null) { out.writeInt(_address.length() * 2); StringUtil.putUnicodeLE(_address, out); } else { out.writeInt(_address.length() * 2 + TAIL_SIZE); StringUtil.putUnicodeLE(_address, out); writeTail(_uninterpretedTail, out); } } else if (FILE_MONIKER.equals(_moniker)) { out.writeShort(_fileOpts); out.writeInt(_shortFilename.length()); StringUtil.putCompressedUnicode(_shortFilename, out); writeTail(_uninterpretedTail, out); if (_address == null) { out.writeInt(0); } else { int addrLen = _address.length() * 2; out.writeInt(addrLen + 6); out.writeInt(addrLen); out.writeShort(0x0003); // TODO const StringUtil.putUnicodeLE(_address, out); } } } if ((_linkOpts & HLINK_PLACE) != 0) { out.writeInt(_textMark.length()); StringUtil.putUnicodeLE(_textMark, out); } } protected int getDataSize() { int size = 0; size += 2 + 2 + 2 + 2; // rwFirst, rwLast, colFirst, colLast size += GUID.ENCODED_SIZE; size += 4; // label_opts size += 4; // link_opts if ((_linkOpts & HLINK_LABEL) != 0) { size += 4; // link length size += _label.length() * 2; } if ((_linkOpts & HLINK_TARGET_FRAME) != 0) { size += 4; // int nChars size += _targetFrame.length() * 2; } if ((_linkOpts & HLINK_URL) != 0 && (_linkOpts & HLINK_UNC_PATH) != 0) { size += 4; // int nChars size += _address.length() * 2; } if ((_linkOpts & HLINK_URL) != 0 && (_linkOpts & HLINK_UNC_PATH) == 0) { size += GUID.ENCODED_SIZE; if (URL_MONIKER.equals(_moniker)) { size += 4; // address length size += _address.length() * 2; if (_uninterpretedTail != null) { size += TAIL_SIZE; } } else if (FILE_MONIKER.equals(_moniker)) { size += 2; // file_opts size += 4; // address length size += _shortFilename.length(); size += TAIL_SIZE; size += 4; if (_address != null) { size += 6; size += _address.length() * 2; } } } if ((_linkOpts & HLINK_PLACE) != 0) { size += 4; // address length size += _textMark.length() * 2; } return size; } private static byte[] readTail(byte[] expectedTail, LittleEndianInput in) { byte[] result = new byte[TAIL_SIZE]; in.readFully(result); if (false) { // Quite a few examples in the unit tests which don't have the exact expected tail for (int i = 0; i < expectedTail.length; i++) { if (expectedTail[i] != result[i]) { System.err.println( "Mismatch in tail byte [" + i + "]" + "expected " + (expectedTail[i] & 0xFF) + " but got " + (result[i] & 0xFF)); } } } return result; } private static void writeTail(byte[] tail, LittleEndianOutput out) { out.write(tail); } public short getSid() { return HyperlinkRecord.sid; } public String toString() { StringBuffer buffer = new StringBuffer(); buffer.append("[HYPERLINK RECORD]\n"); buffer.append(" .range = ").append(_range.formatAsString()).append("\n"); buffer.append(" .guid = ").append(_guid.formatAsString()).append("\n"); buffer.append(" .linkOpts= ").append(HexDump.intToHex(_linkOpts)).append("\n"); buffer.append(" .label = ").append(getLabel()).append("\n"); if ((_linkOpts & HLINK_TARGET_FRAME) != 0) { buffer.append(" .targetFrame= ").append(getTargetFrame()).append("\n"); } if ((_linkOpts & HLINK_URL) != 0 && _moniker != null) { buffer.append(" .moniker = ").append(_moniker.formatAsString()).append("\n"); } if ((_linkOpts & HLINK_PLACE) != 0) { buffer.append(" .textMark= ").append(getTextMark()).append("\n"); } buffer.append(" .address = ").append(getAddress()).append("\n"); buffer.append("[/HYPERLINK RECORD]\n"); return buffer.toString(); } /** Based on the link options, is this a url? */ public boolean isUrlLink() { return (_linkOpts & HLINK_URL) > 0 && (_linkOpts & HLINK_ABS) > 0; } /** Based on the link options, is this a file? */ public boolean isFileLink() { return (_linkOpts & HLINK_URL) > 0 && (_linkOpts & HLINK_ABS) == 0; } /** Based on the link options, is this a document? */ public boolean isDocumentLink() { return (_linkOpts & HLINK_PLACE) > 0; } /** Initialize a new url link */ public void newUrlLink() { _range = new CellRangeAddress(0, 0, 0, 0); _guid = STD_MONIKER; _linkOpts = HLINK_URL | HLINK_ABS | HLINK_LABEL; setLabel(""); _moniker = URL_MONIKER; setAddress(""); _uninterpretedTail = URL_TAIL; } /** Initialize a new file link */ public void newFileLink() { _range = new CellRangeAddress(0, 0, 0, 0); _guid = STD_MONIKER; _linkOpts = HLINK_URL | HLINK_LABEL; _fileOpts = 0; setLabel(""); _moniker = FILE_MONIKER; setAddress(null); setShortFilename(""); _uninterpretedTail = FILE_TAIL; } /** Initialize a new document link */ public void newDocumentLink() { _range = new CellRangeAddress(0, 0, 0, 0); _guid = STD_MONIKER; _linkOpts = HLINK_LABEL | HLINK_PLACE; setLabel(""); _moniker = FILE_MONIKER; setAddress(""); setTextMark(""); } public Object clone() { HyperlinkRecord rec = new HyperlinkRecord(); rec._range = _range.copy(); rec._guid = _guid; rec._linkOpts = _linkOpts; rec._fileOpts = _fileOpts; rec._label = _label; rec._address = _address; rec._moniker = _moniker; rec._shortFilename = _shortFilename; rec._targetFrame = _targetFrame; rec._textMark = _textMark; rec._uninterpretedTail = _uninterpretedTail; return rec; } }
/** * Label Record (0x0204) - read only support for strings stored directly in the cell.. Don't use * this (except to read), use LabelSST instead * * <p>REFERENCE: PG 325 Microsoft Excel 97 Developer's Kit (ISBN: 1-57231-498-2) * * <p> * * @author Andrew C. Oliver (acoliver at apache dot org) * @author Jason Height (jheight at chariot dot net dot au) * @version 2.0-pre * @see org.apache.poi.hssf.record.LabelSSTRecord */ public final class LabelRecord extends Record implements CellValueRecordInterface { private static final POILogger logger = POILogFactory.getLogger(LabelRecord.class); public static final short sid = 0x0204; private int field_1_row; private short field_2_column; private short field_3_xf_index; private short field_4_string_len; private byte field_5_unicode_flag; private String field_6_value; /** Creates new LabelRecord */ public LabelRecord() {} /** @param in the RecordInputstream to read the record from */ public LabelRecord(RecordInputStream in) { field_1_row = in.readUShort(); field_2_column = in.readShort(); field_3_xf_index = in.readShort(); field_4_string_len = in.readShort(); field_5_unicode_flag = in.readByte(); if (field_4_string_len > 0) { if (isUnCompressedUnicode()) { field_6_value = in.readUnicodeLEString(field_4_string_len); } else { field_6_value = in.readCompressedUnicode(field_4_string_len); } } else { field_6_value = ""; } if (in.remaining() > 0) { logger.log( POILogger.INFO, "LabelRecord data remains: " + in.remaining() + " : " + HexDump.toHex(in.readRemainder())); } } /* * READ ONLY ACCESS... THIS IS FOR COMPATIBILITY ONLY...USE LABELSST! public */ public int getRow() { return field_1_row; } public short getColumn() { return field_2_column; } public short getXFIndex() { return field_3_xf_index; } /** * get the number of characters this string contains * * @return number of characters */ public short getStringLength() { return field_4_string_len; } /** * is this uncompressed unicode (16bit)? Or just 8-bit compressed? * * @return isUnicode - True for 16bit- false for 8bit */ public boolean isUnCompressedUnicode() { return (field_5_unicode_flag & 0x01) != 0; } /** * get the value * * @return the text string * @see #getStringLength() */ public String getValue() { return field_6_value; } /** THROWS A RUNTIME EXCEPTION.. USE LABELSSTRecords. YOU HAVE NO REASON to use LABELRecord!! */ public int serialize(int offset, byte[] data) { throw new RecordFormatException("Label Records are supported READ ONLY...convert to LabelSST"); } public int getRecordSize() { throw new RecordFormatException("Label Records are supported READ ONLY...convert to LabelSST"); } public short getSid() { return sid; } public String toString() { StringBuffer sb = new StringBuffer(); sb.append("[LABEL]\n"); sb.append(" .row = ").append(HexDump.shortToHex(getRow())).append("\n"); sb.append(" .column = ").append(HexDump.shortToHex(getColumn())).append("\n"); sb.append(" .xfindex = ").append(HexDump.shortToHex(getXFIndex())).append("\n"); sb.append(" .string_len= ").append(HexDump.shortToHex(field_4_string_len)).append("\n"); sb.append(" .unicode_flag= ").append(HexDump.byteToHex(field_5_unicode_flag)).append("\n"); sb.append(" .value = ").append(getValue()).append("\n"); sb.append("[/LABEL]\n"); return sb.toString(); } /** NO-OP! */ public void setColumn(short col) {} /** NO-OP! */ public void setRow(int row) {} /** no op! */ public void setXFIndex(short xf) {} public Object clone() { LabelRecord rec = new LabelRecord(); rec.field_1_row = field_1_row; rec.field_2_column = field_2_column; rec.field_3_xf_index = field_3_xf_index; rec.field_4_string_len = field_4_string_len; rec.field_5_unicode_flag = field_5_unicode_flag; rec.field_6_value = field_6_value; return rec; } }