Skip to content

Commit a81f6cc

Browse files
committed
Add basic fold parser for PDF content streams
Now in the editor you should be able to freely fold BT->ET blocks and BMC/BDC->EMC sequences.
1 parent 4db7f19 commit a81f6cc

File tree

3 files changed

+240
-4
lines changed

3 files changed

+240
-4
lines changed

src/main/java/com/itextpdf/rups/model/contentstream/ParseTreeNode.java

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,18 @@ public boolean isLeaf() {
187187
return children == null || (children.getNext() == children);
188188
}
189189

190+
/**
191+
* Returns whether text of this node matches the specified text. This
192+
* operation is valid only for primitive nodes.
193+
*
194+
* @param text Expected text.
195+
*
196+
* @return Whether text of this node matches the specified text.
197+
*/
198+
public boolean is(char[] text) {
199+
return Arrays.equals(text, 0, text.length, textArray, textOffset, textOffset + textCount);
200+
}
201+
190202
/**
191203
* Returns whether this is an operator type node with the specified text.
192204
*
@@ -198,10 +210,7 @@ public boolean isOperator(char[] operator) {
198210
if (type != ParseTreeNodeType.OPERATOR) {
199211
return false;
200212
}
201-
return Arrays.equals(
202-
operator, 0, operator.length,
203-
textArray, textOffset, textOffset + textCount
204-
);
213+
return is(operator);
205214
}
206215

207216
/**

src/main/java/com/itextpdf/rups/view/itext/StreamTextEditorPane.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ This file is part of the iText (R) project.
5757
import com.itextpdf.rups.view.contextmenu.ContextMenuMouseListener;
5858
import com.itextpdf.rups.view.contextmenu.StreamPanelContextMenu;
5959
import com.itextpdf.rups.view.itext.editor.Latin1Filter;
60+
import com.itextpdf.rups.view.itext.editor.PdfFoldParser;
6061
import com.itextpdf.rups.view.itext.editor.PdfTokenMaker;
6162
import com.itextpdf.rups.view.itext.editor.PdfTokenPainterFactory;
6263
import com.itextpdf.rups.view.itext.treenodes.PdfObjectTreeNode;
@@ -73,6 +74,7 @@ This file is part of the iText (R) project.
7374
import org.fife.ui.rsyntaxtextarea.RSyntaxDocument;
7475
import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
7576
import org.fife.ui.rsyntaxtextarea.TokenMakerFactory;
77+
import org.fife.ui.rsyntaxtextarea.folding.FoldParserManager;
7678
import org.fife.ui.rtextarea.ExpandedFoldRenderStrategy;
7779
import org.fife.ui.rtextarea.RTextScrollPane;
7880

@@ -114,6 +116,7 @@ public final class StreamTextEditorPane extends RTextScrollPane implements IRups
114116
final AbstractTokenMakerFactory tokenMakerFactory =
115117
(AbstractTokenMakerFactory) TokenMakerFactory.getDefaultInstance();
116118
tokenMakerFactory.putMapping(MIME_PDF, PdfTokenMaker.class.getName());
119+
FoldParserManager.get().addFoldParserMapping(MIME_PDF, new PdfFoldParser());
117120
/*
118121
* There doesn't seem to be a good way to detect, whether you can call
119122
* setData on a PdfStream or not in advance. It cannot be called if a
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
/*
2+
This file is part of the iText (R) project.
3+
Copyright (c) 1998-2024 Apryse Group NV
4+
Authors: Apryse Software.
5+
6+
This program is free software; you can redistribute it and/or modify
7+
it under the terms of the GNU Affero General Public License version 3
8+
as published by the Free Software Foundation with the addition of the
9+
following permission added to Section 15 as permitted in Section 7(a):
10+
FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
11+
APRYSE GROUP. APRYSE GROUP DISCLAIMS THE WARRANTY OF NON INFRINGEMENT
12+
OF THIRD PARTY RIGHTS
13+
14+
This program is distributed in the hope that it will be useful, but
15+
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
16+
or FITNESS FOR A PARTICULAR PURPOSE.
17+
See the GNU Affero General Public License for more details.
18+
You should have received a copy of the GNU Affero General Public License
19+
along with this program; if not, see http://www.gnu.org/licenses or write to
20+
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
21+
Boston, MA, 02110-1301 USA, or download the license from the following URL:
22+
http://itextpdf.com/terms-of-use/
23+
24+
The interactive user interfaces in modified source and object code versions
25+
of this program must display Appropriate Legal Notices, as required under
26+
Section 5 of the GNU Affero General Public License.
27+
28+
In accordance with Section 7(b) of the GNU Affero General Public License,
29+
a covered work must retain the producer line in every PDF that is created
30+
or manipulated using iText.
31+
32+
You can be released from the requirements of the license by purchasing
33+
a commercial license. Buying such a license is mandatory as soon as you
34+
develop commercial activities involving the iText software without
35+
disclosing the source code of your own applications.
36+
These activities include: offering paid services to customers as an ASP,
37+
serving PDFs on the fly in a web application, shipping iText with a closed
38+
source product.
39+
40+
For more information, please contact iText Software Corp. at this
41+
42+
*/
43+
package com.itextpdf.rups.view.itext.editor;
44+
45+
import com.itextpdf.rups.model.LoggerHelper;
46+
import com.itextpdf.rups.model.contentstream.ParseTreeNode;
47+
import com.itextpdf.rups.model.contentstream.ParseTreeNodeType;
48+
import com.itextpdf.rups.model.contentstream.PdfContentStreamParser;
49+
import com.itextpdf.rups.model.contentstream.PdfOperators;
50+
import com.itextpdf.rups.view.Language;
51+
52+
import java.util.ArrayDeque;
53+
import java.util.ArrayList;
54+
import java.util.Collections;
55+
import java.util.Deque;
56+
import java.util.Iterator;
57+
import java.util.List;
58+
import javax.swing.text.BadLocationException;
59+
import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
60+
import org.fife.ui.rsyntaxtextarea.folding.Fold;
61+
import org.fife.ui.rsyntaxtextarea.folding.FoldParser;
62+
import org.fife.ui.rsyntaxtextarea.folding.FoldType;
63+
64+
/**
65+
* Fold parser for handling PDF content streams.
66+
*/
67+
public final class PdfFoldParser implements FoldParser {
68+
/**
69+
* Default size to use for the marker stack.
70+
*/
71+
private static final int DEFAULT_MARKER_STACK_SIZE = 8;
72+
/**
73+
* Marker for a marked content sequence fold.
74+
*/
75+
private static final Object MARKED_CONTENT = new Object();
76+
/**
77+
* Marked for a text object block fold.
78+
*/
79+
private static final Object TEXT_OBJECT = new Object();
80+
81+
/**
82+
* Pre-allocated content stream parser.
83+
*/
84+
private final PdfContentStreamParser parser = new PdfContentStreamParser();
85+
86+
@Override
87+
public List<Fold> getFolds(RSyntaxTextArea textArea) {
88+
try {
89+
return getFoldsInternal(textArea);
90+
} catch (BadLocationException e) {
91+
LoggerHelper.error(Language.ERROR_UNEXPECTED_EXCEPTION.getString(), e, getClass());
92+
return Collections.emptyList();
93+
}
94+
}
95+
96+
private List<Fold> getFoldsInternal(RSyntaxTextArea textArea)
97+
throws BadLocationException {
98+
/*
99+
* TODO: We shouldn't constantly re-parse this...
100+
*
101+
* We should have the parse tree stored next to the text area and only
102+
* update it, when text area is changed.
103+
*/
104+
final ParseTreeNode root = parseText(textArea);
105+
final State state = new State();
106+
final Iterator<ParseTreeNode> it = root.primitiveNodeIterator();
107+
while (it.hasNext()) {
108+
final ParseTreeNode node = it.next();
109+
if (node.getType() != ParseTreeNodeType.OPERATOR) {
110+
continue;
111+
}
112+
if (node.is(PdfOperators.BT)) {
113+
// Text object block start
114+
startNewFold(state, textArea, TEXT_OBJECT, node);
115+
} else if (inTextObject(state) && node.is(PdfOperators.ET)) {
116+
// Text object block end
117+
endCurrentFold(state, node);
118+
} else if (node.is(PdfOperators.BMC) || node.is(PdfOperators.BDC)) {
119+
// Marked content sequence start
120+
startNewFold(state, textArea, MARKED_CONTENT, node);
121+
} else if (inMarkedContent(state) && node.is(PdfOperators.EMC)) {
122+
// Marked content sequence end
123+
endCurrentFold(state, node);
124+
}
125+
// TODO: Add more blocks (like less stable q/Q)
126+
}
127+
return state.folds;
128+
}
129+
130+
private ParseTreeNode parseText(RSyntaxTextArea textArea) {
131+
parser.reset();
132+
parser.append(textArea.getText());
133+
return parser.result();
134+
}
135+
136+
/**
137+
* Starts a new child fold of the specified type.
138+
*
139+
* @param state Current folding algorithm state.
140+
* @param textArea The text area whose contents should be analyzed.
141+
* @param marker Type marker.
142+
* @param node Node where fold starts.
143+
*/
144+
private static void startNewFold(State state, RSyntaxTextArea textArea, Object marker, ParseTreeNode node)
145+
throws BadLocationException {
146+
if (state.currentFold != null) {
147+
state.currentFold = state.currentFold.createChild(FoldType.CODE, node.getTextOffset());
148+
} else {
149+
state.currentFold = new Fold(FoldType.CODE, textArea, node.getTextOffset());
150+
state.folds.add(state.currentFold);
151+
}
152+
state.markers.push(marker);
153+
}
154+
155+
/**
156+
* Ends the current fold and sets current fold to parent. If current fold
157+
* spans only one line, it will be deleted.
158+
*
159+
* @param state Current folding algorithm state.
160+
* @param node Node where fold ends.
161+
*/
162+
private static void endCurrentFold(State state, ParseTreeNode node)
163+
throws BadLocationException {
164+
if (state.currentFold == null) {
165+
return;
166+
}
167+
state.currentFold.setEndOffset(node.getTextOffset());
168+
// If it is on a single line, we skip it
169+
if (state.currentFold.isOnSingleLine()) {
170+
removeCurrentFold(state);
171+
} else {
172+
state.currentFold = state.currentFold.getParent();
173+
state.markers.pop();
174+
}
175+
}
176+
177+
/**
178+
* Removes the current fold and set its parent as the new current fold.
179+
*
180+
* @param state Current folding algorithm state.
181+
*/
182+
private static void removeCurrentFold(State state) {
183+
if (state.currentFold == null) {
184+
return;
185+
}
186+
final Fold parent = state.currentFold.getParent();
187+
if (!state.currentFold.removeFromParent()) {
188+
state.folds.remove(state.folds.size() - 1);
189+
}
190+
state.currentFold = parent;
191+
state.markers.pop();
192+
}
193+
194+
/**
195+
* Returns whether we are inside a marked content sequence fold.
196+
*
197+
* @param state Current folding algorithm state.
198+
*
199+
* @return Whether we are inside a marked content sequence fold.
200+
*/
201+
private static boolean inMarkedContent(State state) {
202+
return MARKED_CONTENT == state.markers.peek();
203+
}
204+
205+
/**
206+
* Returns whether we are inside a text object block fold.
207+
*
208+
* @param state Current folding algorithm state.
209+
*
210+
* @return Whether we are inside a text object block fold.
211+
*/
212+
private static boolean inTextObject(State state) {
213+
return TEXT_OBJECT == state.markers.peek();
214+
}
215+
216+
/**
217+
* Folding algorithm state.
218+
*/
219+
private static final class State {
220+
private final List<Fold> folds = new ArrayList<>();
221+
private final Deque<Object> markers = new ArrayDeque<>(DEFAULT_MARKER_STACK_SIZE);
222+
private Fold currentFold = null;
223+
}
224+
}

0 commit comments

Comments
 (0)