From 8de5ebb35fe083aa33da4adb81d2e7d925f352fe Mon Sep 17 00:00:00 2001 From: Yegor Kozlov Date: Wed, 30 May 2007 11:56:46 +0000 Subject: [PATCH] HSLF: readonly support for hyperlinks git-svn-id: https://svn.apache.org/repos/asf/jakarta/poi/trunk@542804 13f79535-47bb-0310-9956-ffa450edef68 --- .../content/xdocs/hslf/how-to-shapes.xml | 80 ++++++--- .../apache/poi/hslf/examples/Hyperlinks.java | 80 +++++++++ .../org/apache/poi/hslf/model/Hyperlink.java | 168 ++++++++++++++++++ .../src/org/apache/poi/hslf/model/Shape.java | 13 +- .../org/apache/poi/hslf/model/ShapeGroup.java | 9 + .../src/org/apache/poi/hslf/model/Sheet.java | 21 +++ .../org/apache/poi/hslf/model/TextBox.java | 68 +++---- .../org/apache/poi/hslf/model/TextRun.java | 30 ++++ .../poi/hslf/record/EscherTextboxWrapper.java | 15 ++ .../apache/poi/hslf/record/ExHyperlink.java | 21 ++- .../org/apache/poi/hslf/record/ExObjList.java | 17 ++ .../poi/hslf/record/InteractiveInfoAtom.java | 155 ++++++++++++++-- .../org/apache/poi/hslf/record/PPDrawing.java | 7 + .../apache/poi/hslf/record/RecordTypes.java | 2 +- .../hslf/record/TxInteractiveInfoAtom.java | 122 +++++++++++++ .../apache/poi/hslf/model/TestHyperlink.java | 89 ++++++++++ .../org/apache/poi/hslf/model/TestShapes.java | 3 +- .../poi/hslf/record/TestInteractiveInfo.java | 10 +- .../hslf/record/TestInteractiveInfoAtom.java | 26 +-- .../record/TestTxInteractiveInfoAtom.java | 110 ++++++++++++ 20 files changed, 936 insertions(+), 110 deletions(-) create mode 100644 src/scratchpad/examples/src/org/apache/poi/hslf/examples/Hyperlinks.java create mode 100644 src/scratchpad/src/org/apache/poi/hslf/model/Hyperlink.java create mode 100644 src/scratchpad/src/org/apache/poi/hslf/record/TxInteractiveInfoAtom.java create mode 100644 src/scratchpad/testcases/org/apache/poi/hslf/model/TestHyperlink.java create mode 100644 src/scratchpad/testcases/org/apache/poi/hslf/record/TestTxInteractiveInfoAtom.java diff --git a/src/documentation/content/xdocs/hslf/how-to-shapes.xml b/src/documentation/content/xdocs/hslf/how-to-shapes.xml index 74e921f8fe..df40776a53 100644 --- a/src/documentation/content/xdocs/hslf/how-to-shapes.xml +++ b/src/documentation/content/xdocs/hslf/how-to-shapes.xml @@ -38,6 +38,7 @@
  • How to set slide title
  • How to work with slide/shape background
  • How to create bulleted lists
  • +
  • Hyperlinks
  • Features @@ -323,31 +324,68 @@
    How to create bulleted lists - SlideShow ppt = new SlideShow(); + SlideShow ppt = new SlideShow(); - Slide slide = ppt.createSlide(); + Slide slide = ppt.createSlide(); - TextBox shape = new TextBox(); - RichTextRun rt = shape.getTextRun().getRichTextRuns()[0]; - shape.setText( - "January\r" + - "February\r" + - "March\r" + - "April"); - rt.setFontSize(42); - rt.setBullet(true); - rt.setBulletOffset(0); //bullet offset - rt.setTextOffset(50); //text offset (should be greater than bullet offset) - rt.setBulletChar('\u263A'); //bullet character - slide.addShape(shape); + TextBox shape = new TextBox(); + RichTextRun rt = shape.getTextRun().getRichTextRuns()[0]; + shape.setText( + "January\r" + + "February\r" + + "March\r" + + "April"); + rt.setFontSize(42); + rt.setBullet(true); + rt.setBulletOffset(0); //bullet offset + rt.setTextOffset(50); //text offset (should be greater than bullet offset) + rt.setBulletChar('\u263A'); //bullet character + slide.addShape(shape); - shape.setAnchor(new java.awt.Rectangle(50, 50, 500, 300)); //position of the text box in the slide - slide.addShape(shape); + shape.setAnchor(new java.awt.Rectangle(50, 50, 500, 300)); //position of the text box in the slide + slide.addShape(shape); - FileOutputStream out = new FileOutputStream("bullets.ppt"); - ppt.write(out); - out.close(); - + FileOutputStream out = new FileOutputStream("bullets.ppt"); + ppt.write(out); + out.close(); + +
    + +
    How to read hyperlinks from a slide show + + FileInputStream is = new FileInputStream("slideshow.ppt"); + SlideShow ppt = new SlideShow(is); + is.close(); + + Slide[] slide = ppt.getSlides(); + for (int j = 0; j < slide.length; j++) { + + //read hyperlinks from the text runs + TextRun[] txt = slide[j].getTextRuns(); + for (int k = 0; k < txt.length; k++) { + String text = txt[k].getText(); + Hyperlink[] links = txt[k].getHyperlinks(); + if(links != null) for (int l = 0; l < links.length; l++) { + Hyperlink link = links[l]; + String title = link.getTitle(); + String address = link.getAddress(); + String substring = text.substring(link.getStartIndex(), link.getEndIndex()-1); //in ppt end index is inclusive + } + } + + //in PowerPoint you can assign a hyperlink to a shape without text, + //for example to a Line object. The code below demonstrates how to + //read such hyperlinks + Shape[] sh = slide[j].getShapes(); + for (int k = 0; k < sh.length; k++) { + Hyperlink link = sh[k].getHyperlink(); + if(link != null) { + String title = link.getTitle(); + String address = link.getAddress(); + } + } + } +
    diff --git a/src/scratchpad/examples/src/org/apache/poi/hslf/examples/Hyperlinks.java b/src/scratchpad/examples/src/org/apache/poi/hslf/examples/Hyperlinks.java new file mode 100644 index 0000000000..f919e545ff --- /dev/null +++ b/src/scratchpad/examples/src/org/apache/poi/hslf/examples/Hyperlinks.java @@ -0,0 +1,80 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ +package org.apache.poi.hslf.examples; + +import org.apache.poi.hslf.usermodel.SlideShow; +import org.apache.poi.hslf.model.Slide; +import org.apache.poi.hslf.model.TextRun; +import org.apache.poi.hslf.model.Hyperlink; +import org.apache.poi.hslf.model.Shape; + +import java.io.FileInputStream; + +/** + * Demonstrates how to read hyperlinks from a presentation + * + * @author Yegor Kozlov + */ +public class Hyperlinks { + + public static void main(String[] args) throws Exception { + for (int i = 0; i < args.length; i++) { + FileInputStream is = new FileInputStream(args[i]); + SlideShow ppt = new SlideShow(is); + is.close(); + + Slide[] slide = ppt.getSlides(); + for (int j = 0; j < slide.length; j++) { + System.out.println("slide " + slide[j].getSlideNumber()); + + //read hyperlinks from the slide's text runs + System.out.println("reading hyperlinks from the text runs"); + TextRun[] txt = slide[j].getTextRuns(); + for (int k = 0; k < txt.length; k++) { + String text = txt[k].getText(); + Hyperlink[] links = txt[k].getHyperlinks(); + if(links != null) for (int l = 0; l < links.length; l++) { + Hyperlink link = links[l]; + String title = link.getTitle(); + String address = link.getAddress(); + System.out.println(" " + title); + System.out.println(" " + address); + String substring = text.substring(link.getStartIndex(), link.getEndIndex()-1);//in ppt end index is inclusive + System.out.println(" " + substring); + } + } + + //in PowerPoint you can assign a hyperlink to a shape without text, + //for example to a Line object. The code below demonstrates how to + //read such hyperlinks + System.out.println(" reading hyperlinks from the slide's shapes"); + Shape[] sh = slide[j].getShapes(); + for (int k = 0; k < sh.length; k++) { + Hyperlink link = sh[k].getHyperlink(); + if(link != null) { + String title = link.getTitle(); + String address = link.getAddress(); + System.out.println(" " + title); + System.out.println(" " + address); + } + } + } + + } + + } +} diff --git a/src/scratchpad/src/org/apache/poi/hslf/model/Hyperlink.java b/src/scratchpad/src/org/apache/poi/hslf/model/Hyperlink.java new file mode 100644 index 0000000000..21a4dc59c4 --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hslf/model/Hyperlink.java @@ -0,0 +1,168 @@ + +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hslf.model; + +import org.apache.poi.hslf.record.*; +import org.apache.poi.hslf.usermodel.SlideShow; +import org.apache.poi.ddf.EscherContainerRecord; +import org.apache.poi.ddf.EscherRecord; +import org.apache.poi.ddf.EscherClientDataRecord; + +import java.util.ArrayList; +import java.util.List; +import java.util.Iterator; + +/** + * Represents a hyperlink in a PowerPoint document + * + * @author Yegor Kozlov + */ +public class Hyperlink { + + private int type; + private String address; + private String title; + private int startIndex, endIndex; + + /** + * Gets the type of the hyperlink action. + * Must be a ACTION_* constant defined in InteractiveInfoAtom + * + * @return the hyperlink URL + * @see InteractiveInfoAtom + */ + public int getType() { + return type; + } + + /** + * Gets the hyperlink URL + * + * @return the hyperlink URL + */ + public String getAddress() { + return address; + } + + /** + * Gets the hyperlink user-friendly title (if different from URL) + * + * @return the hyperlink user-friendly title + */ + public String getTitle() { + return title; + } + + /** + * Gets the beginning character position + * + * @return the beginning character position + */ + public int getStartIndex() { + return startIndex; + } + + /** + * Gets the ending character position + * + * @return the ending character position + */ + public int getEndIndex() { + return endIndex; + } + + /** + * Find hyperlinks in a text run + * + * @param run TextRun to lookup hyperlinks in + * @return found hyperlinks or null if not found + */ + protected static Hyperlink[] find(TextRun run){ + ArrayList lst = new ArrayList(); + SlideShow ppt = run.getSheet().getSlideShow(); + //document-level container which stores info about all links in a presentation + ExObjList exobj = ppt.getDocumentRecord().getExObjList(); + if (exobj == null) { + return null; + } + Record[] records = run._records; + if(records != null) find(records, exobj, lst); + + Hyperlink[] links = null; + if (lst.size() > 0){ + links = new Hyperlink[lst.size()]; + lst.toArray(links); + } + return links; + } + + /** + * Find hyperlink assigned to the supplied shape + * + * @param shape Shape to lookup hyperlink in + * @return found hyperlink or null + */ + protected static Hyperlink find(Shape shape){ + ArrayList lst = new ArrayList(); + SlideShow ppt = shape.getSheet().getSlideShow(); + //document-level container which stores info about all links in a presentation + ExObjList exobj = ppt.getDocumentRecord().getExObjList(); + if (exobj == null) { + return null; + } + + EscherContainerRecord spContainer = shape.getSpContainer(); + List spchild = spContainer.getChildRecords(); + for (Iterator it = spchild.iterator(); it.hasNext(); ) { + EscherRecord obj = (EscherRecord)it.next(); + if (obj.getRecordId() == EscherClientDataRecord.RECORD_ID){ + byte[] data = ((EscherContainerRecord)obj).serialize(); + Record[] records = Record.findChildRecords(data, 8, data.length-8); + if(records != null) find(records, exobj, lst); + } + } + + return lst.size() == 1 ? (Hyperlink)lst.get(0) : null; + } + + private static void find(Record[] records, ExObjList exobj, List out){ + for (int i = 0; i < records.length; i++) { + //see if we have InteractiveInfo in the textrun's records + if( records[i] instanceof InteractiveInfo){ + InteractiveInfo hldr = (InteractiveInfo)records[i]; + InteractiveInfoAtom info = hldr.getInteractiveInfoAtom(); + int id = info.getHyperlinkID(); + ExHyperlink linkRecord = exobj.get(id); + if (linkRecord != null){ + Hyperlink link = new Hyperlink(); + link.title = linkRecord.getLinkTitle(); + link.address = linkRecord.getLinkURL(); + link.type = info.getAction(); + + if (++i < records.length && records[i] instanceof TxInteractiveInfoAtom){ + TxInteractiveInfoAtom txinfo = (TxInteractiveInfoAtom)records[i]; + link.startIndex = txinfo.getStartIndex(); + link.endIndex = txinfo.getEndIndex(); + } + out.add(link); + } + } + } + } +} diff --git a/src/scratchpad/src/org/apache/poi/hslf/model/Shape.java b/src/scratchpad/src/org/apache/poi/hslf/model/Shape.java index 65827bf94d..ffe89e03df 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/model/Shape.java +++ b/src/scratchpad/src/org/apache/poi/hslf/model/Shape.java @@ -45,7 +45,7 @@ public abstract class Shape { // For logging protected POILogger logger = POILogFactory.getLogger(this.getClass()); - + /** * In Escher absolute distances are specified in * English Metric Units (EMUs), occasionally referred to as A units; @@ -309,4 +309,15 @@ public abstract class Shape { return new Fill(this); } + + /** + * Returns the hyperlink assigned to this shape + * + * @return the hyperlink assigned to this shape + * or null if not found. + */ + public Hyperlink getHyperlink(){ + return Hyperlink.find(this); + } + } diff --git a/src/scratchpad/src/org/apache/poi/hslf/model/ShapeGroup.java b/src/scratchpad/src/org/apache/poi/hslf/model/ShapeGroup.java index d474a22f6f..aeb34763ec 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/model/ShapeGroup.java +++ b/src/scratchpad/src/org/apache/poi/hslf/model/ShapeGroup.java @@ -211,4 +211,13 @@ public class ShapeGroup extends Shape{ return spRecord.getOptions() >> 4; } + /** + * Returns null - shape groups can't have hyperlinks + * + * @return null. + */ + public Hyperlink getHyperlink(){ + return null; + } + } diff --git a/src/scratchpad/src/org/apache/poi/hslf/model/Sheet.java b/src/scratchpad/src/org/apache/poi/hslf/model/Sheet.java index c53f7f9edd..b1761b2d9a 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/model/Sheet.java +++ b/src/scratchpad/src/org/apache/poi/hslf/model/Sheet.java @@ -21,6 +21,7 @@ package org.apache.poi.hslf.model; import org.apache.poi.ddf.EscherContainerRecord; import org.apache.poi.ddf.EscherDgRecord; import org.apache.poi.ddf.EscherRecord; +import org.apache.poi.ddf.EscherSpRecord; import org.apache.poi.hslf.record.*; import org.apache.poi.hslf.usermodel.SlideShow; @@ -127,7 +128,13 @@ public abstract class Sheet { Vector runsV = new Vector(); EscherTextboxWrapper[] wrappers = ppdrawing.getTextboxWrappers(); for (int i = 0; i < wrappers.length; i++) { + int s1 = runsV.size(); findTextRuns(wrappers[i].getChildRecords(), runsV); + int s2 = runsV.size(); + if (s2 != s1){ + TextRun t = (TextRun) runsV.get(runsV.size()-1); + t.setShapeId(wrappers[i].getShapeId()); + } } TextRun[] runs = new TextRun[runsV.size()]; for (int i = 0; i < runs.length; i++) { @@ -176,6 +183,15 @@ public abstract class Sheet { } if (trun != null) { + ArrayList lst = new ArrayList(); + for (int j = i; j < records.length; j++) { + if(j > i && records[j] instanceof TextHeaderAtom) break; + lst.add(records[j]); + } + Record[] recs = new Record[lst.size()]; + lst.toArray(recs); + trun._records = recs; + found.add(trun); i++; } else { @@ -232,6 +248,11 @@ public abstract class Sheet { EscherDgRecord dg = (EscherDgRecord) Shape.getEscherChild(dgContainer, EscherDgRecord.RECORD_ID); dg.setNumShapes(dg.getNumShapes() + 1); + int shapeId = dg.getLastMSOSPID()+1; + dg.setLastMSOSPID(shapeId); + + EscherSpRecord sp = shape.getSpContainer().getChildById(EscherSpRecord.RECORD_ID); + if(sp != null) sp.setShapeId(shapeId); shape.setSheet(this); shape.afterInsert(this); diff --git a/src/scratchpad/src/org/apache/poi/hslf/model/TextBox.java b/src/scratchpad/src/org/apache/poi/hslf/model/TextBox.java index 82d9a4de7f..2d5c866f77 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/model/TextBox.java +++ b/src/scratchpad/src/org/apache/poi/hslf/model/TextBox.java @@ -28,7 +28,6 @@ import java.awt.*; import java.awt.font.FontRenderContext; import java.awt.font.TextLayout; import java.io.IOException; -import java.util.Vector; /** * Represents a TextFrame shape in PowerPoint. @@ -170,7 +169,7 @@ public class TextBox extends SimpleShape { * @return the text string for this textbox. */ public String getText(){ - return _txtrun.getText(); + return _txtrun == null ? null : _txtrun.getText(); } /** @@ -452,56 +451,37 @@ public class TextBox extends SimpleShape { } private void initTextRun(){ - TextHeaderAtom tha = null; - TextCharsAtom tca = null; - TextBytesAtom tba = null; - StyleTextPropAtom sta = null; OutlineTextRefAtom ota = null; // Find the interesting child records Record[] child = _txtbox.getChildRecords(); for (int i = 0; i < child.length; i++) { - if (child[i] instanceof TextHeaderAtom) tha = (TextHeaderAtom)child[i]; - else if (child[i] instanceof TextBytesAtom) tba = (TextBytesAtom)child[i]; - else if (child[i] instanceof StyleTextPropAtom) sta = (StyleTextPropAtom)child[i]; - else if (child[i] instanceof OutlineTextRefAtom) ota = (OutlineTextRefAtom)child[i]; - else if (child[i] instanceof TextCharsAtom) tca = (TextCharsAtom)child[i]; - } - - // Special handling for cases where there's an OutlineTextRefAtom - if (ota != null) { - // TextHeaderAtom, TextBytesAtom and StyleTextPropAtom are - // stored outside of EscherContainerRecord - int idx = ota.getTextIndex(); - Slide sl = (Slide)getSheet(); - Record[] rec = sl.getSlideAtomsSet().getSlideRecords(); - for (int i = 0, j = 0; i < rec.length; i++) { - if(rec[i].getRecordType() == RecordTypes.TextHeaderAtom.typeID){ - if(j++ == idx) { //we found j-th TextHeaderAtom, read the text data - for (int k = i; k < rec.length; k++) { - if (rec[k] instanceof TextHeaderAtom) { - if (tha != null) break; - else tha = (TextHeaderAtom)rec[k]; - } - else if (rec[k] instanceof TextBytesAtom) tba = (TextBytesAtom)rec[k]; - else if (rec[k] instanceof TextCharsAtom) tca = (TextCharsAtom)rec[k]; - else if (rec[k] instanceof StyleTextPropAtom) sta = (StyleTextPropAtom)rec[k]; - } - } - } + if (child[i] instanceof OutlineTextRefAtom) { + ota = (OutlineTextRefAtom)child[i]; + break; } } - - // If we found the records we needed, create a TextRun - if(tba != null) { - // Bytes based Text Run - _txtrun = new TextRun(tha,tba,sta); - } else if (tca != null) { - // Characters (unicode) based Text Run - _txtrun = new TextRun(tha,tca,sta); + + Sheet sheet = getSheet(); + TextRun[] runs = sheet.getTextRuns(); + if (ota != null) { + int idx = ota.getTextIndex(); + if(idx < runs.length) _txtrun = runs[idx]; + if(_txtrun == null) { + logger.log(POILogger.WARN, "text run not found for OutlineTextRefAtom.TextIndex=" + idx); + } } else { - // Empty text box - logger.log(POILogger.WARN, "no text records found for TextBox"); + int shapeId = _escherContainer.getChildById(EscherSpRecord.RECORD_ID).getShapeId(); + if(runs != null) for (int i = 0; i < runs.length; i++) { + if(runs[i].getShapeId() == shapeId){ + _txtrun = runs[i]; + break; + } + } + if(_txtrun == null) { + logger.log(POILogger.WARN, "text run not found for shapeId=" + shapeId); + } } + } } diff --git a/src/scratchpad/src/org/apache/poi/hslf/model/TextRun.java b/src/scratchpad/src/org/apache/poi/hslf/model/TextRun.java index 8e6ae61627..7770311477 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/model/TextRun.java +++ b/src/scratchpad/src/org/apache/poi/hslf/model/TextRun.java @@ -49,6 +49,12 @@ public class TextRun protected RichTextRun[] _rtRuns; private SlideShow slideShow; private Sheet sheet; + private int shapeId; + /** + * all text run records that follow TextHeaderAtom. + * (there can be misc InteractiveInfo, TxInteractiveInfo and other records) + */ + protected Record[] _records; /** * Constructs a Text Run from a Unicode text block @@ -517,4 +523,28 @@ public class TextRun public Sheet getSheet(){ return this.sheet; } + + /** + * @return Shape ID + */ + protected int getShapeId(){ + return shapeId; + } + + /** + * @param id Shape ID + */ + protected void setShapeId(int id){ + shapeId = id; + } + + /** + * Returns the array of all hyperlinks in this text run + * + * @return the array of all hyperlinks in this text run + * or null if not found. + */ + public Hyperlink[] getHyperlinks(){ + return Hyperlink.find(this); + } } diff --git a/src/scratchpad/src/org/apache/poi/hslf/record/EscherTextboxWrapper.java b/src/scratchpad/src/org/apache/poi/hslf/record/EscherTextboxWrapper.java index 4dd0d76ab1..142eee88fc 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/record/EscherTextboxWrapper.java +++ b/src/scratchpad/src/org/apache/poi/hslf/record/EscherTextboxWrapper.java @@ -39,6 +39,7 @@ public class EscherTextboxWrapper extends RecordContainer { private EscherTextboxRecord _escherRecord; private long _type; + private int shapeId; /** * Returns the underlying DDF Escher Record @@ -93,4 +94,18 @@ public class EscherTextboxWrapper extends RecordContainer // Save in the escher layer _escherRecord.setData(data); } + + /** + * @return Shape ID + */ + public int getShapeId(){ + return shapeId; + } + + /** + * @param id Shape ID + */ + public void setShapeId(int id){ + shapeId = id; + } } diff --git a/src/scratchpad/src/org/apache/poi/hslf/record/ExHyperlink.java b/src/scratchpad/src/org/apache/poi/hslf/record/ExHyperlink.java index 8ccc28fb9d..8ba58cdb61 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/record/ExHyperlink.java +++ b/src/scratchpad/src/org/apache/poi/hslf/record/ExHyperlink.java @@ -42,13 +42,22 @@ public class ExHyperlink extends RecordContainer { /** * Returns the URL of the link. - * TODO: Figure out which of detailsA or detailsB is the - * one that always holds it + * + * @return the URL of the link */ public String getLinkURL() { - return linkDetailsA.getText(); + return linkDetailsB == null ? null : linkDetailsB.getText(); } - + + /** + * Returns the hyperlink's user-readable name + * + * @return the hyperlink's user-readable name + */ + public String getLinkTitle() { + return linkDetailsA == null ? null : linkDetailsA.getText(); + } + /** * Sets the URL of the link * TODO: Figure out if we should always set both @@ -66,13 +75,13 @@ public class ExHyperlink extends RecordContainer { * Get the link details (field A) */ public String _getDetailsA() { - return linkDetailsA.getText(); + return linkDetailsA == null ? null : linkDetailsA.getText(); } /** * Get the link details (field B) */ public String _getDetailsB() { - return linkDetailsB.getText(); + return linkDetailsB == null ? null : linkDetailsB.getText(); } /** diff --git a/src/scratchpad/src/org/apache/poi/hslf/record/ExObjList.java b/src/scratchpad/src/org/apache/poi/hslf/record/ExObjList.java index 95fe5c967c..a511ef6095 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/record/ExObjList.java +++ b/src/scratchpad/src/org/apache/poi/hslf/record/ExObjList.java @@ -109,4 +109,21 @@ public class ExObjList extends RecordContainer { writeOut(_header[0],_header[1],_type,_children,out); } + /** + * Lookup a hyperlink by its unique id + * + * @param id hyperlink id + * @return found ExHyperlink or null + */ + public ExHyperlink get(int id){ + for(int i=0; i<_children.length; i++) { + if(_children[i] instanceof ExHyperlink) { + ExHyperlink rec = (ExHyperlink)_children[i]; + if (rec.getExHyperlinkAtom().getNumber() == id){ + return rec; + } + } + } + return null; + } } diff --git a/src/scratchpad/src/org/apache/poi/hslf/record/InteractiveInfoAtom.java b/src/scratchpad/src/org/apache/poi/hslf/record/InteractiveInfoAtom.java index 40200d1301..8943f882d8 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/record/InteractiveInfoAtom.java +++ b/src/scratchpad/src/org/apache/poi/hslf/record/InteractiveInfoAtom.java @@ -30,10 +30,37 @@ import org.apache.poi.util.LittleEndian; * (The actual link is held Document.ExObjList.ExHyperlink) * * @author Nick Burch + * @author Yegor Kozlov */ public class InteractiveInfoAtom extends RecordAtom { + + /** + * Action Table + */ + public static final int ACTION_NONE = 0; + public static final int ACTION_MACRO = 1; + public static final int ACTION_RUNPROGRAM = 2; + public static final int ACTION_JUMP = 3; + public static final int ACTION_HYPERLINK = 4; + public static final int ACTION_OLE = 5; + public static final int ACTION_MEDIA = 6; + public static final int ACTION_CUSTOMSHOW = 7; + + /** + * Jump Table + */ + public static final int JUMP_NONE = 0; + public static final int JUMP_NEXTSLIDE = 1; + public static final int JUMP_PREVIOUSSLIDE = 2; + public static final int JUMP_FIRSTSLIDE = 3; + public static final int JUMP_LASTSLIDE = 4; + public static final int JUMP_LASTSLIDEVIEWED = 5; + public static final int JUMP_ENDSHOW = 6; + + + /** * Record header. */ @@ -90,46 +117,138 @@ public class InteractiveInfoAtom extends RecordAtom * ExHyperlink with this number to get the details. * @return the link number */ - public int getNumber() { + public int getHyperlinkID() { return LittleEndian.getInt(_data,4); } /** - * Sets the link number - * @param number the link number. + * Sets the persistent unique identifier of the link + * + * @param number the persistent unique identifier of the link */ - public void setNumber(int number) { + public void setHyperlinkID(int number) { LittleEndian.putInt(_data,4,number); } /** - * Get the first number - meaning unknown + * a reference to a sound in the sound collection. */ - public int _getNumber1() { + public int getSoundRef() { return LittleEndian.getInt(_data,0); } - protected void _setNumber1(int val) { + /** + * a reference to a sound in the sound collection. + * + * @param val a reference to a sound in the sound collection + */ + public void setSoundRef(int val) { LittleEndian.putInt(_data, 0, val); } /** - * Get the third number - meaning unknown + * Hyperlink Action. + *

    + * see ACTION_* constants for the list of actions + *

    + * + * @return hyperlink action. */ - public int _getNumber3() { - return LittleEndian.getInt(_data,8); - } - protected void _setNumber3(int val) { - LittleEndian.putInt(_data, 8, val); + public byte getAction() { + return _data[8]; } /** - * Get the fourth number - meaning unknown + * Hyperlink Action + *

    + * see ACTION_* constants for the list of actions + *

    + * + * @param val hyperlink action. */ - public int _getNumber4() { - return LittleEndian.getInt(_data,12); + public void setAction(byte val) { + _data[8] = val; } - protected void _setNumber4(int val) { - LittleEndian.putInt(_data, 12, val); + + /** + * Only valid when action == OLEAction. OLE verb to use, 0 = first verb, 1 = second verb, etc. + */ + public byte getOleVerb() { + return _data[9]; + } + + /** + * Only valid when action == OLEAction. OLE verb to use, 0 = first verb, 1 = second verb, etc. + */ + public void setOleVerb(byte val) { + _data[9] = val; + } + + /** + * Jump + *

    + * see JUMP_* constants for the list of actions + *

    + * + * @return jump + */ + public byte getJump() { + return _data[10]; + } + + /** + * Jump + *

    + * see JUMP_* constants for the list of actions + *

    + * + * @param val jump + */ + public void setJump(byte val) { + _data[10] = val; + } + + /** + * Flags + *

    + *

  • Bit 1: Animated. If 1, then button is animated + *
  • Bit 2: Stop sound. If 1, then stop current sound when button is pressed. + *
  • Bit 3: CustomShowReturn. If 1, and this is a jump to custom show, + * then return to this slide after custom show. + *

    + */ + public byte getFlags() { + return _data[11]; + } + + /** + * Flags + *

    + *

  • Bit 1: Animated. If 1, then button is animated + *
  • Bit 2: Stop sound. If 1, then stop current sound when button is pressed. + *
  • Bit 3: CustomShowReturn. If 1, and this is a jump to custom show, + * then return to this slide after custom show. + *

    + */ + public void setFlags(byte val) { + _data[11] = val; + } + + /** + * hyperlink type + * + * @return hyperlink type + */ + public byte getHyperlinkType() { + return _data[12]; + } + + /** + * hyperlink type + * + * @param val hyperlink type + */ + public void setHyperlinkType(byte val) { + _data[12] = val; } /** diff --git a/src/scratchpad/src/org/apache/poi/hslf/record/PPDrawing.java b/src/scratchpad/src/org/apache/poi/hslf/record/PPDrawing.java index e34e9d841f..db0d434aee 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/record/PPDrawing.java +++ b/src/scratchpad/src/org/apache/poi/hslf/record/PPDrawing.java @@ -147,6 +147,13 @@ public class PPDrawing extends RecordAtom EscherTextboxRecord tbr = (EscherTextboxRecord)toSearch[i]; EscherTextboxWrapper w = new EscherTextboxWrapper(tbr); found.add(w); + for (int j = i; j >= 0; j--) { + if(toSearch[j] instanceof EscherSpRecord){ + EscherSpRecord sp = (EscherSpRecord)toSearch[j]; + w.setShapeId(sp.getShapeId()); + break; + } + } } else { // If it has children, walk them if(toSearch[i].isContainerRecord()) { diff --git a/src/scratchpad/src/org/apache/poi/hslf/record/RecordTypes.java b/src/scratchpad/src/org/apache/poi/hslf/record/RecordTypes.java index 3183bd553a..3c8d3aa41c 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/record/RecordTypes.java +++ b/src/scratchpad/src/org/apache/poi/hslf/record/RecordTypes.java @@ -112,7 +112,7 @@ public class RecordTypes { public static final Type SlideNumberMCAtom = new Type(4056,null); public static final Type HeadersFooters = new Type(4057,null); public static final Type HeadersFootersAtom = new Type(4058,null); - public static final Type TxInteractiveInfoAtom = new Type(4063,null); + public static final Type TxInteractiveInfoAtom = new Type(4063,TxInteractiveInfoAtom.class); public static final Type CharFormatAtom = new Type(4066,null); public static final Type ParaFormatAtom = new Type(4067,null); public static final Type RecolorInfoAtom = new Type(4071,null); diff --git a/src/scratchpad/src/org/apache/poi/hslf/record/TxInteractiveInfoAtom.java b/src/scratchpad/src/org/apache/poi/hslf/record/TxInteractiveInfoAtom.java new file mode 100644 index 0000000000..b3db6bafaa --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hslf/record/TxInteractiveInfoAtom.java @@ -0,0 +1,122 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ +package org.apache.poi.hslf.record; + +import org.apache.poi.util.LittleEndian; + +import java.io.OutputStream; +import java.io.IOException; + +/** + * Tne atom that holds starting and ending character positions of a hyperlink + * + * @author Yegor Kozlov + */ +public class TxInteractiveInfoAtom extends RecordAtom { + /** + * Record header. + */ + private byte[] _header; + + /** + * Record data. + */ + private byte[] _data; + + /** + * Constructs a brand new link related atom record. + */ + protected TxInteractiveInfoAtom() { + _header = new byte[8]; + _data = new byte[8]; + + LittleEndian.putShort(_header, 2, (short)getRecordType()); + LittleEndian.putInt(_header, 4, _data.length); + } + + /** + * Constructs the link related atom record from its + * source data. + * + * @param source the source data as a byte array. + * @param start the start offset into the byte array. + * @param len the length of the slice in the byte array. + */ + protected TxInteractiveInfoAtom(byte[] source, int start, int len) { + // Get the header. + _header = new byte[8]; + System.arraycopy(source,start,_header,0,8); + + // Get the record data. + _data = new byte[len-8]; + System.arraycopy(source,start+8,_data,0,len-8); + + } + + /** + * Gets the beginning character position + * + * @return the beginning character position + */ + public int getStartIndex() { + return LittleEndian.getInt(_data, 0); + } + + /** + * Sets the beginning character position + * @param idx the beginning character position + */ + public void setStartIndex(int idx) { + LittleEndian.putInt(_data, 0, idx); + } + + /** + * Gets the ending character position + * + * @return the ending character position + */ + public int getEndIndex() { + return LittleEndian.getInt(_data, 4); + } + + /** + * Sets the ending character position + * + * @param idx the ending character position + */ + public void setEndIndex(int idx) { + LittleEndian.putInt(_data, 4, idx); + } + + /** + * Gets the record type. + * @return the record type. + */ + public long getRecordType() { return RecordTypes.TxInteractiveInfoAtom.typeID; } + + /** + * Write the contents of the record back, so it can be written + * to disk + * + * @param out the output stream to write to. + * @throws java.io.IOException if an error occurs. + */ + public void writeOut(OutputStream out) throws IOException { + out.write(_header); + out.write(_data); + } +} diff --git a/src/scratchpad/testcases/org/apache/poi/hslf/model/TestHyperlink.java b/src/scratchpad/testcases/org/apache/poi/hslf/model/TestHyperlink.java new file mode 100644 index 0000000000..9108d3c36c --- /dev/null +++ b/src/scratchpad/testcases/org/apache/poi/hslf/model/TestHyperlink.java @@ -0,0 +1,89 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ +package org.apache.poi.hslf.model; + +import junit.framework.TestCase; + +import java.io.FileInputStream; +import java.io.File; + +import org.apache.poi.hslf.usermodel.SlideShow; + +/** + * Test Hyperlink. + * + * @author Yegor Kozlov + */ +public class TestHyperlink extends TestCase { + protected String cwd = System.getProperty("HSLF.testdata.path"); + + public void testTextRunHyperlinks() throws Exception { + FileInputStream is = new FileInputStream(new File(cwd, "WithLinks.ppt")); + SlideShow ppt = new SlideShow(is); + is.close(); + + TextRun[] run; + Slide slide; + slide = ppt.getSlides()[0]; + run = slide.getTextRuns(); + for (int i = 0; i < run.length; i++) { + String text = run[i].getText(); + if (text.equals( + "This page has two links:\n" + + "http://jakarta.apache.org/poi/\n" + + "\n" + + "http://slashdot.org/\n" + + "\n" + + "In addition, its notes has one link")){ + + Hyperlink[] links = run[i].getHyperlinks(); + assertNotNull(links); + assertEquals(2, links.length); + + assertEquals("http://jakarta.apache.org/poi/", links[0].getTitle()); + assertEquals("http://jakarta.apache.org/poi/", links[0].getAddress()); + assertEquals("http://jakarta.apache.org/poi/", text.substring(links[0].getStartIndex(), links[0].getEndIndex()-1)); + + assertEquals("http://slashdot.org/", links[1].getTitle()); + assertEquals("http://slashdot.org/", links[1].getAddress()); + assertEquals("http://slashdot.org/", text.substring(links[1].getStartIndex(), links[1].getEndIndex()-1)); + + } + } + + slide = ppt.getSlides()[1]; + run = slide.getTextRuns(); + for (int i = 0; i < run.length; i++) { + String text = run[i].getText(); + if (text.equals( + "I have the one link:\n" + + "Jakarta HSSF")){ + + Hyperlink[] links = run[i].getHyperlinks(); + assertNotNull(links); + assertEquals(1, links.length); + + assertEquals("http://jakarta.apache.org/poi/hssf/", links[0].getTitle()); + assertEquals("http://jakarta.apache.org/poi/hssf/", links[0].getAddress()); + assertEquals("Jakarta HSSF", text.substring(links[0].getStartIndex(), links[0].getEndIndex()-1)); + + } + } + + } + +} diff --git a/src/scratchpad/testcases/org/apache/poi/hslf/model/TestShapes.java b/src/scratchpad/testcases/org/apache/poi/hslf/model/TestShapes.java index 4bd1678983..fd637774c0 100644 --- a/src/scratchpad/testcases/org/apache/poi/hslf/model/TestShapes.java +++ b/src/scratchpad/testcases/org/apache/poi/hslf/model/TestShapes.java @@ -161,7 +161,8 @@ public class TestShapes extends TestCase { out.close(); ppt = new SlideShow(new HSLFSlideShow(new ByteArrayInputStream(out.toByteArray()))); - + sl = ppt.getSlides()[0]; + txtbox = (TextBox)sl.getShapes()[0]; rt = txtbox.getTextRun().getRichTextRuns()[0]; diff --git a/src/scratchpad/testcases/org/apache/poi/hslf/record/TestInteractiveInfo.java b/src/scratchpad/testcases/org/apache/poi/hslf/record/TestInteractiveInfo.java index d3d61fbb9d..ad48bd1dcd 100644 --- a/src/scratchpad/testcases/org/apache/poi/hslf/record/TestInteractiveInfo.java +++ b/src/scratchpad/testcases/org/apache/poi/hslf/record/TestInteractiveInfo.java @@ -48,7 +48,7 @@ public class TestInteractiveInfo extends TestCase { InteractiveInfo ii = new InteractiveInfo(data_a, 0, data_a.length); InteractiveInfoAtom ia = ii.getInteractiveInfoAtom(); - assertEquals(1, ia.getNumber()); + assertEquals(1, ia.getHyperlinkID()); } public void testWrite() throws Exception { @@ -69,10 +69,10 @@ public class TestInteractiveInfo extends TestCase { InteractiveInfoAtom ia = ii.getInteractiveInfoAtom(); // Set values - ia.setNumber(1); - ia._setNumber1(0); - ia._setNumber3(4); - ia._setNumber4(8); + ia.setHyperlinkID(1); + ia.setSoundRef(0); + ia.setAction((byte)4); + ia.setHyperlinkType((byte)8); // Check it's now the same as a ByteArrayOutputStream baos = new ByteArrayOutputStream(); diff --git a/src/scratchpad/testcases/org/apache/poi/hslf/record/TestInteractiveInfoAtom.java b/src/scratchpad/testcases/org/apache/poi/hslf/record/TestInteractiveInfoAtom.java index d001ac35de..df945f5a22 100644 --- a/src/scratchpad/testcases/org/apache/poi/hslf/record/TestInteractiveInfoAtom.java +++ b/src/scratchpad/testcases/org/apache/poi/hslf/record/TestInteractiveInfoAtom.java @@ -53,22 +53,22 @@ public class TestInteractiveInfoAtom extends TestCase { InteractiveInfoAtom ia = new InteractiveInfoAtom(data_a, 0, data_a.length); InteractiveInfoAtom ib = new InteractiveInfoAtom(data_b, 0, data_b.length); - assertEquals(1, ia.getNumber()); - assertEquals(4, ib.getNumber()); + assertEquals(1, ia.getHyperlinkID()); + assertEquals(4, ib.getHyperlinkID()); } public void testGetRest() throws Exception { InteractiveInfoAtom ia = new InteractiveInfoAtom(data_a, 0, data_a.length); InteractiveInfoAtom ib = new InteractiveInfoAtom(data_b, 0, data_b.length); - assertEquals(0, ia._getNumber1()); - assertEquals(0, ib._getNumber1()); + assertEquals(0, ia.getSoundRef()); + assertEquals(0, ib.getSoundRef()); - assertEquals(4, ia._getNumber3()); - assertEquals(4, ib._getNumber3()); + assertEquals(4, ia.getAction()); + assertEquals(4, ib.getAction()); - assertEquals(8, ia._getNumber4()); - assertEquals(8, ib._getNumber4()); + assertEquals(8, ia.getHyperlinkType()); + assertEquals(8, ib.getHyperlinkType()); } public void testWrite() throws Exception { @@ -88,10 +88,10 @@ public class TestInteractiveInfoAtom extends TestCase { InteractiveInfoAtom ia = new InteractiveInfoAtom(); // Set values - ia.setNumber(1); - ia._setNumber1(0); - ia._setNumber3(4); - ia._setNumber4(8); + ia.setHyperlinkID(1); + ia.setSoundRef(0); + ia.setAction((byte)4); + ia.setHyperlinkType((byte)8); // Check it's now the same as a ByteArrayOutputStream baos = new ByteArrayOutputStream(); @@ -109,7 +109,7 @@ public class TestInteractiveInfoAtom extends TestCase { InteractiveInfoAtom ia = new InteractiveInfoAtom(data_a, 0, data_a.length); // Change the number - ia.setNumber(4); + ia.setHyperlinkID(4); // Check bytes are now the same ByteArrayOutputStream baos = new ByteArrayOutputStream(); diff --git a/src/scratchpad/testcases/org/apache/poi/hslf/record/TestTxInteractiveInfoAtom.java b/src/scratchpad/testcases/org/apache/poi/hslf/record/TestTxInteractiveInfoAtom.java new file mode 100644 index 0000000000..9cb53d762f --- /dev/null +++ b/src/scratchpad/testcases/org/apache/poi/hslf/record/TestTxInteractiveInfoAtom.java @@ -0,0 +1,110 @@ + +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + + + +package org.apache.poi.hslf.record; + + +import junit.framework.TestCase; +import java.io.ByteArrayOutputStream; +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * Tests that TxInteractiveInfoAtom works properly. + * + * @author Yegor Kozlov + */ +public class TestTxInteractiveInfoAtom extends TestCase { + // From WithLinks.ppt + private byte[] data_a = new byte[] { + 00, 00, (byte)0xDF, 0x0F, 0x08, 00, 00, 00, + 0x19, 00, 00, 00, 0x38, 00, 00, 00 + }; + + private byte[] data_b = new byte[] { + 00, 00, (byte)0xDF, 0x0F, 0x08, 00, 00, 00, + 0x39, 00, 00, 00, 0x4E, 00, 00, 00 + }; + + public void testRead() throws Exception { + TxInteractiveInfoAtom ia1 = new TxInteractiveInfoAtom(data_a, 0, data_a.length); + + assertEquals(4063, ia1.getRecordType()); + assertEquals(25, ia1.getStartIndex()); + assertEquals(56, ia1.getEndIndex()); + + TxInteractiveInfoAtom ia2 = new TxInteractiveInfoAtom(data_b, 0, data_b.length); + + assertEquals(4063, ia2.getRecordType()); + assertEquals(57, ia2.getStartIndex()); + assertEquals(78, ia2.getEndIndex()); + } + + public void testWrite() throws Exception { + TxInteractiveInfoAtom atom = new TxInteractiveInfoAtom(data_a, 0, data_a.length); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + atom.writeOut(baos); + byte[] b = baos.toByteArray(); + + assertEquals(data_a.length, b.length); + for(int i=0; i