diff --git a/wcag-validation/src/main/java/org/verapdf/gf/model/factory/chunks/ChunkParser.java b/wcag-validation/src/main/java/org/verapdf/gf/model/factory/chunks/ChunkParser.java index c577739a3..d57158812 100644 --- a/wcag-validation/src/main/java/org/verapdf/gf/model/factory/chunks/ChunkParser.java +++ b/wcag-validation/src/main/java/org/verapdf/gf/model/factory/chunks/ChunkParser.java @@ -413,7 +413,7 @@ public void parseChunk(Operator rawOperator, List arguments, int operat case Operators.F_FILL_OBSOLETE: case Operators.F_STAR_FILL: processh(); - processf(); + processf(operatorIndex); break; case Operators.GS: PDExtGState extGState = this.resourcesHandler.getExtGState(getLastCOSName(arguments)); @@ -491,21 +491,21 @@ public void parseChunk(Operator rawOperator, List arguments, int operat case Operators.B_CLOSEPATH_FILL_STROKE: case Operators.B_STAR_CLOSEPATH_EOFILL_STROKE: processh(); - processB(); + processB(operatorIndex); break; case Operators.B_FILL_STROKE: case Operators.B_STAR_EOFILL_STROKE: - processB(); + processB(operatorIndex); break; case Operators.N: nonDrawingArtifacts = new ArrayList<>(); break; case Operators.S_CLOSE_STROKE: processh(); - processS(); + processS(operatorIndex); break; case Operators.S_STROKE: - processS(); + processS(operatorIndex); break; case Operators.CM_CONCAT: if (arguments.size() == 6) { @@ -611,7 +611,12 @@ private void processh() { path.setCurrentPoint(path.getStartX(), path.getStartY()); } - private void processB() { + /** + * @param operatorIndex position of the paint operator in the stream, so a + * line art chunk can carry it the way text and image + * chunks already do + */ + private void processB(int operatorIndex) { if (!processLayers()) { nonDrawingArtifacts = new ArrayList<>(); return; @@ -622,26 +627,31 @@ private void processB() { if (chunk instanceof LineChunk) { LineChunk lineChunk = transformLineChunk((LineChunk)chunk, graphicsState.getLineWidth(), graphicsState.getLineCap()); - processLineChunk(boundingBox, mcid, lineChunk); + processLineChunk(boundingBox, mcid, lineChunk, operatorIndex); } else if (chunk instanceof CurveChunk) { CurveChunk curveChunk = CurveChunk.transformCurve((CurveChunk)chunk, graphicsState.getCTM(), graphicsState.getLineWidth()); - processBoundingBox(boundingBox, mcid, curveChunk.getBoundingBox()); + processBoundingBox(boundingBox, mcid, curveChunk.getBoundingBox(), operatorIndex); } else if (chunk instanceof Rectangle) { LineChunk line = ((Rectangle)chunk).getLine(graphicsState.getLineWidth()); if (line != null) { LineChunk line1 = transformLineChunk(line, line.getWidth(), LineChunk.PROJECTING_SQUARE_CAP_STYLE); - processLineChunk(boundingBox, mcid, line1); + processLineChunk(boundingBox, mcid, line1, operatorIndex); } } } if (StaticStorages.getIsIgnoreMCIDs()) { - lineArtContainer.add(mcid, boundingBox); + lineArtContainer.add(mcid, boundingBox, operatorIndex, xObjectName); } nonDrawingArtifacts = new ArrayList<>(); } - private void processS() { + /** + * @param operatorIndex position of the paint operator in the stream, so a + * line art chunk can carry it the way text and image + * chunks already do + */ + private void processS(int operatorIndex) { if (!processLayers()) { nonDrawingArtifacts = new ArrayList<>(); return; @@ -652,11 +662,11 @@ private void processS() { if (chunk instanceof LineChunk) { LineChunk lineChunk = transformLineChunk((LineChunk)chunk, graphicsState.getLineWidth(), graphicsState.getLineCap()); - processLineChunk(boundingBox, mcid, lineChunk); + processLineChunk(boundingBox, mcid, lineChunk, operatorIndex); } else if (chunk instanceof CurveChunk) { CurveChunk curveChunk = CurveChunk.transformCurve((CurveChunk)chunk, graphicsState.getCTM(), graphicsState.getLineWidth()); - processBoundingBox(boundingBox, mcid, curveChunk.getBoundingBox()); + processBoundingBox(boundingBox, mcid, curveChunk.getBoundingBox(), operatorIndex); } else if (chunk instanceof Rectangle) { Rectangle rectangle = (Rectangle) chunk; if (rectangle.getHeight() < graphicsState.getLineWidth() || @@ -664,24 +674,29 @@ private void processS() { LineChunk line = rectangle.getLine(graphicsState.getLineWidth()); if (line != null) { LineChunk line1 = transformLineChunk(line, line.getWidth(), LineChunk.PROJECTING_SQUARE_CAP_STYLE); - processLineChunk(boundingBox, mcid, line1); + processLineChunk(boundingBox, mcid, line1, operatorIndex); } } else { List lines = rectangle.getLines(graphicsState.getLineWidth()); for (LineChunk line : lines) { LineChunk line1 = transformLineChunk(line, graphicsState.getLineWidth(), LineChunk.PROJECTING_SQUARE_CAP_STYLE); - processLineChunk(boundingBox, mcid, line1); + processLineChunk(boundingBox, mcid, line1, operatorIndex); } } } } if (StaticStorages.getIsIgnoreMCIDs()) { - lineArtContainer.add(mcid, boundingBox); + lineArtContainer.add(mcid, boundingBox, operatorIndex, xObjectName); } nonDrawingArtifacts = new ArrayList<>(); } - private void processf() { + /** + * @param operatorIndex position of the paint operator in the stream, so a + * line art chunk can carry it the way text and image + * chunks already do + */ + private void processf(int operatorIndex) { if (!processLayers()) { nonDrawingArtifacts = new ArrayList<>(); return; @@ -694,42 +709,48 @@ private void processf() { LineChunk line = ((Rectangle)chunk).getLine(0); if (line != null) { LineChunk line1 = transformLineChunk(line, line.getWidth(), LineChunk.PROJECTING_SQUARE_CAP_STYLE); - processLineChunk(boundingBox, mcid, line1); + processLineChunk(boundingBox, mcid, line1, operatorIndex); } } else if (chunk instanceof LineChunk) { LineChunk line = parsingRectangleFromLines(i); if (line != null) { - processLineChunk(boundingBox, mcid, line); + processLineChunk(boundingBox, mcid, line, operatorIndex); i += 3; } else { LineChunk line1 = transformLineChunk((LineChunk)chunk, graphicsState.getLineWidth(), graphicsState.getLineCap()); - processBoundingBox(boundingBox, mcid, line1.getBoundingBox()); + processBoundingBox(boundingBox, mcid, line1.getBoundingBox(), operatorIndex); } } else if (chunk instanceof CurveChunk) { CurveChunk curveChunk = CurveChunk.transformCurve((CurveChunk)chunk, graphicsState.getCTM(), graphicsState.getLineWidth()); - processBoundingBox(boundingBox, mcid, curveChunk.getBoundingBox()); + processBoundingBox(boundingBox, mcid, curveChunk.getBoundingBox(), operatorIndex); } } if (StaticStorages.getIsIgnoreMCIDs()) { - lineArtContainer.add(mcid, boundingBox); + lineArtContainer.add(mcid, boundingBox, operatorIndex, xObjectName); } nonDrawingArtifacts = new ArrayList<>(); } - private void processLineChunk(BoundingBox boundingBox, Long mcid, LineChunk lineChunk) { - lineArtContainer.add(mcid, lineChunk); + private void processLineChunk(BoundingBox boundingBox, Long mcid, LineChunk lineChunk, + int operatorIndex) { + lineArtContainer.add(mcid, lineChunk, operatorIndex, xObjectName); if (StaticStorages.getIsIgnoreMCIDs()) { boundingBox.union(lineChunk.getBoundingBox()); } } - private void processBoundingBox(BoundingBox boundingBox, Long mcid, BoundingBox newBoundingBox) { + private void processBoundingBox(BoundingBox boundingBox, Long mcid, BoundingBox newBoundingBox, + int operatorIndex) { if (StaticStorages.getIsIgnoreMCIDs()) { boundingBox.union(newBoundingBox); } else { - lineArtContainer.add(mcid, newBoundingBox); + // Curves reach the container here rather than through + // processLineChunk, so the operator index has to be carried on this + // path too: a region drawn only with curve operators is otherwise + // left without one. + lineArtContainer.add(mcid, newBoundingBox, operatorIndex, xObjectName); } } @@ -1086,9 +1107,18 @@ public void parseLineArts() { if (lineChunks == null) { lineChunks = new LinkedList<>(); } + // Stream position recorded for this mcid while the operators were read. + // The chunks below are built after the stream, so this is the only way + // they can carry one — and without one a vector region can never be + // given a marked content id, which is what makes it untaggable. + List lineArtStreamInfos = lineArtContainer.getStreamInfos(mcid); if (mcid == null && parentMarkedContent == null) { for (BoundingBox box : boundingBoxes.getValue()) { - artifacts.add(new LineArtChunk(box)); + LineArtChunk artifact = new LineArtChunk(box); + for (StreamInfo info : lineArtStreamInfos) { + artifact.getStreamInfos().add(new StreamInfo(info)); + } + artifacts.add(artifact); } artifacts.addAll(lineChunks); } @@ -1097,10 +1127,25 @@ public void parseLineArts() { boundingBox.union(box); } if (mcid != null) { - lineArtContainer.getLineArt(mcid).setBoundingBox(boundingBox); - lineArtContainer.getLineArt(mcid).setLineChunks(lineChunks); + LineArtChunk existing = lineArtContainer.getLineArt(mcid); + existing.setBoundingBox(boundingBox); + existing.setLineChunks(lineChunks); + // Set here rather than where the chunk was created. The chunk is + // created on the region's first bounding box, when only the first + // operator has been seen, and the operators that follow never + // reach it — so a region inside marked content exposed just one of + // them. This runs once the stream has been read, so the list is + // complete. + existing.getStreamInfos().clear(); + for (StreamInfo info : lineArtStreamInfos) { + existing.getStreamInfos().add(new StreamInfo(info)); + } } else { - StaticStorages.getChunks().add(parentObjectKey, parentMarkedContent, new LineArtChunk(boundingBox, lineChunks)); + LineArtChunk lineArtChunk = new LineArtChunk(boundingBox, lineChunks); + for (StreamInfo info : lineArtStreamInfos) { + lineArtChunk.getStreamInfos().add(new StreamInfo(info)); + } + StaticStorages.getChunks().add(parentObjectKey, parentMarkedContent, lineArtChunk); } } } @@ -1109,18 +1154,33 @@ public void processLineArts() { if (!StaticStorages.getIsIgnoreMCIDs()) { return; } + // Read before the container's record is cleared below. This is the path a + // region outside any marked content takes, and the chunks it produces are + // the ones a consumer sees, so a position missing here is a region that + // cannot be tagged however well the rest is wired. + List pending = lineArtContainer.getStreamInfos(null); List lineChunks = lineArtContainer.getLineChunks(null); if (lineChunks != null && !lineChunks.isEmpty()) { + for (LineChunk lineChunk : lineChunks) { + for (StreamInfo info : pending) { + lineChunk.getStreamInfos().add(new StreamInfo(info)); + } + } artifacts.addAll(lineChunks); lineChunks.clear(); } List boundingBoxes = lineArtContainer.getBoundingBoxes(null); if (boundingBoxes != null && !boundingBoxes.isEmpty()) { for (BoundingBox box : boundingBoxes) { - artifacts.add(new LineArtChunk(box)); + LineArtChunk artifact = new LineArtChunk(box); + for (StreamInfo info : pending) { + artifact.getStreamInfos().add(new StreamInfo(info)); + } + artifacts.add(artifact); } boundingBoxes.clear(); } + lineArtContainer.clearStreamInfos(null); } public boolean processLayers() { diff --git a/wcag-validation/src/main/java/org/verapdf/gf/model/factory/chunks/LineArtContainer.java b/wcag-validation/src/main/java/org/verapdf/gf/model/factory/chunks/LineArtContainer.java index 5451d405e..3c6746830 100644 --- a/wcag-validation/src/main/java/org/verapdf/gf/model/factory/chunks/LineArtContainer.java +++ b/wcag-validation/src/main/java/org/verapdf/gf/model/factory/chunks/LineArtContainer.java @@ -25,6 +25,8 @@ import org.verapdf.wcag.algorithms.entities.content.LineArtChunk; import org.verapdf.wcag.algorithms.entities.content.LineChunk; import org.verapdf.wcag.algorithms.entities.geometry.BoundingBox; +import org.verapdf.wcag.algorithms.semanticalgorithms.containers.StaticContainers; +import org.verapdf.wcag.algorithms.semanticalgorithms.utils.StreamInfo; import java.util.*; @@ -35,12 +37,22 @@ public class LineArtContainer { private final Map> lineArtBBoxes; private final Map lineArts; private final Map> lineArtLines; + /** + * Stream position of the first paint operator seen for each mcid. + * + *

Kept because the chunk for an untagged region is not built here — it is + * built in {@code ChunkParser.parseLineArts}, once the stream has been read, + * where no single operator is in scope any more. Without somewhere to hold + * the position, a region whose marks carry no mcid can never be given one. + */ + private final Map> lineArtStreamInfos; private final COSKey objectKey; public LineArtContainer(COSKey objectKey) { lineArtBBoxes = new HashMap<>(); lineArts = new HashMap<>(); lineArtLines = new HashMap<>(); + lineArtStreamInfos = new HashMap<>(); this.objectKey = objectKey; } @@ -56,7 +68,31 @@ public LineArtChunk getLineArt(Long mcid) { return lineArts.get(mcid); } + /** Stream positions recorded for this mcid, empty when none were. */ + public List getStreamInfos(Long mcid) { + List infos = lineArtStreamInfos.get(mcid); + return infos == null ? Collections.emptyList() : infos; + } + + /** + * Forgets the position for this mcid, so the next region under it records its + * own. Called where the boxes are cleared: keeping the old position would + * hand a later region the operator that drew an earlier one. + */ + public void clearStreamInfos(Long mcid) { + lineArtStreamInfos.remove(mcid); + } + public void add(Long mcid, LineChunk lineChunk) { + add(mcid, lineChunk, null, null); + } + + /** + * @param operatorIndex index of the paint operator that drew this line, or + * null when it is not known + * @param xObjectName name of the form the line was drawn inside, or null + */ + public void add(Long mcid, LineChunk lineChunk, Integer operatorIndex, String xObjectName) { List lineChunks = getLineChunks(mcid); if (lineChunks != null) { lineChunks.add(lineChunk); @@ -65,13 +101,27 @@ public void add(Long mcid, LineChunk lineChunk) { lineChunks.add(lineChunk); lineArtLines.put(mcid, lineChunks); } - add(mcid, lineChunk.getBoundingBox()); + add(mcid, lineChunk.getBoundingBox(), operatorIndex, xObjectName); } public void add(Long mcid, BoundingBox boundingBox) { + add(mcid, boundingBox, null, null); + } + + public void add(Long mcid, BoundingBox boundingBox, Integer operatorIndex, String xObjectName) { if (boundingBox.isEmpty()) { return; } + // Every paint operator, not just the first. A StreamInfo names one + // operator, so a region an infographic draws with thousands of them needs + // one entry each: recording only the first wrapped one operator and left + // the rest of the region outside the tree. + if (StaticContainers.isDataLoader() && operatorIndex != null) { + List infos = lineArtStreamInfos.computeIfAbsent(mcid, k -> new ArrayList<>()); + if (infos.isEmpty() || infos.get(infos.size() - 1).getOperatorIndex() != operatorIndex) { + infos.add(new StreamInfo(operatorIndex, xObjectName, null)); + } + } List list = getBoundingBoxes(mcid); if (list != null && !StaticStorages.getIsIgnoreMCIDs()) { boolean isSeparateBoundingBox = true; @@ -90,6 +140,9 @@ public void add(Long mcid, BoundingBox boundingBox) { } else { if (mcid != null) { LineArtChunk lineArtChunk = new LineArtChunk(); + // No stream info attached here on purpose: only the region's first + // operator has been seen at this point. ChunkParser.parseLineArts + // sets the complete list once the stream has been read. StaticStorages.getChunks().add(objectKey, mcid, lineArtChunk); lineArts.put(mcid, lineArtChunk); }