Skip to content

Commit

Permalink
add a pretty hacky subfigure plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
multun committed Jun 17, 2020
1 parent 2ff5aa8 commit 20aa671
Show file tree
Hide file tree
Showing 4 changed files with 164 additions and 41 deletions.
2 changes: 1 addition & 1 deletion src/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
# number figures
numfig = True
numfig_secnum_depth = 2
numfig_format = {'figure': 'Figure %s', 'table': 'Table %s', 'code-block': 'Code Block %s'}
numfig_format = {'figure': 'Figure %s', 'table': 'Table %s', 'code-block': 'Code Block %s', "subfigure": "Figure %s"}

# only parse rst files
source_suffix = ".rst"
Expand Down
12 changes: 6 additions & 6 deletions src/exclusive-or.rst
Original file line number Diff line number Diff line change
Expand Up @@ -221,37 +221,37 @@ with :numref:`fig-multitimepad`.
:label: fig-multitimepad
:width: 0.48

.. figure:: ./Illustrations/KeyReuse/Broken.png
.. subfigure:: ./Illustrations/KeyReuse/Broken.png
:alt:
:align: center

First plaintext.

.. figure:: ./Illustrations/KeyReuse/Crypto.png
.. subfigure:: ./Illustrations/KeyReuse/Crypto.png
:alt:
:align: center

Second plaintext.

.. figure:: ./Illustrations/KeyReuse/BrokenEncrypted.png
.. subfigure:: ./Illustrations/KeyReuse/BrokenEncrypted.png
:alt:
:align: center

First ciphertext.

.. figure:: ./Illustrations/KeyReuse/CryptoEncrypted.png
.. subfigure:: ./Illustrations/KeyReuse/CryptoEncrypted.png
:alt:
:align: center

Second ciphertext.

.. figure:: ./Illustrations/KeyReuse/Key.png
.. subfigure:: ./Illustrations/KeyReuse/Key.png
:alt:
:align: center

Reused key.

.. figure:: ./Illustrations/KeyReuse/CiphertextsXOR.png
.. subfigure:: ./Illustrations/KeyReuse/CiphertextsXOR.png
:alt:
:align: center

Expand Down
14 changes: 7 additions & 7 deletions src/stream-ciphers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -74,39 +74,39 @@ image [#]_. We'll then visually inspect the different outputs.
:width: 0.48

.. _fig-ECBDemoPlaintext:
.. figure:: ./Illustrations/ECB/Plaintext.png
.. subfigure:: ./Illustrations/ECB/Plaintext.png
:alt: Plaintext image
:align: center

Plaintext image, 2000 by 1400 pixels, 24 bit color depth.

.. _fig-ECBDemo5px:
.. figure:: ./Illustrations/ECB/Ciphertext5.png
.. subfigure:: ./Illustrations/ECB/Ciphertext5.png
:alt: ECB mode ciphertext, 5 pixel (120 bit) block size.
:align: center

ECB mode ciphertext, 5 pixel (120 bit) block size.

.. figure:: ./Illustrations/ECB/Ciphertext30.png
.. subfigure:: ./Illustrations/ECB/Ciphertext30.png
:alt: ECB mode ciphertext, 30 pixel (720 bit) block size.
:align: center

ECB mode ciphertext, 30 pixel (720 bit) block size.

.. figure:: ./Illustrations/ECB/Ciphertext100.png
.. subfigure:: ./Illustrations/ECB/Ciphertext100.png
:alt: ECB mode ciphertext, 100 pixel (2400 bit) block size.
:align: center

ECB mode ciphertext, 100 pixel (2400 bit) block size.

.. figure:: ./Illustrations/ECB/Ciphertext400.png
.. subfigure:: ./Illustrations/ECB/Ciphertext400.png
:alt: ECB mode ciphertext, 400 pixel (9600 bit) block size.
:align: center

ECB mode ciphertext, 400 pixel (9600 bit) block size.

.. _fig-ECBDemoIdealizedCiphertext:
.. figure:: ./Illustrations/ECB/Random.png
.. subfigure:: ./Illustrations/ECB/Random.png
:alt: Ciphertext under idealized encryption.
:align: center

Expand All @@ -117,7 +117,7 @@ image [#]_. We'll then visually inspect the different outputs.
Information about the macro-structure of the image clearly leaks.
This becomes less apparent as block sizes increase, but only at
block sizes far larger than typical block ciphers. Only the first
block size (figure :ref:`fig-ECBDemoIdealizedCiphertext`, a block size of 5
block size (:numref:`fig-ECBDemoIdealizedCiphertext`, a block size of 5
pixels or 120 bits) is realistic.


Expand Down
177 changes: 150 additions & 27 deletions src/subfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,22 +34,37 @@
from docutils import nodes
import docutils.parsers.rst.directives as directives
from docutils.parsers.rst import Directive
from sphinx.directives.patches import Figure

from typing import Any, Dict, List, Set, Tuple, TypeVar
from typing import cast

class subfig(nodes.General, nodes.Element):
from docutils import nodes
from docutils.nodes import Element, Node

from sphinx.application import Sphinx
from sphinx.environment import BuildEnvironment
from sphinx.environment.collectors import EnvironmentCollector
from sphinx.locale import __
from sphinx.transforms import SphinxContentsFilter
from sphinx.util import url_re, logging

class subfigure(nodes.figure):
pass


def skip_visit(self, node):
raise nodes.SkipNode


def visit_subfig_tex(self, node):
def visit_subfigure_tex(self, node):
self.__body = self.body
self.body = []
self.visit_figure(node)


def depart_subfig_tex(self, node):
def depart_subfigure_tex(self, node):
self.depart_figure(node)
figoutput = "".join(self.body)
# the newlines at the beginning mess up the subfigure alignment
# a newline between two subfigures means "stop stacking subfigures"
Expand All @@ -73,12 +88,40 @@ def depart_subfig_tex(self, node):
self.body.append(figoutput)


def visit_subfig_html(self, node):
def visit_subfigure_html(self, node):
self.__body = self.body
self.body = []

# change the way fignumbers are displayed inside subfigures
def patched_add_fignumber(node):
def append_fignumber(figtype, figure_id):
if self.builder.name == 'singlehtml':
key = "%s/%s" % (self.docnames[-1], figtype)
else:
key = figtype

if figure_id in self.builder.fignumbers.get(key, {}):
self.body.append('<span class="caption-number">')
numbers = self.builder.fignumbers[key][figure_id]
self.body.append("(%s) " % numbers[-1])
self.body.append('</span>')

figtype = self.builder.env.domains['std'].get_enumerable_node_type(node)
if figtype:
if len(node['ids']) == 0:
msg = __('Any IDs not assigned for %s node') % node.tagname
logger.warning(msg, location=node)
else:
append_fignumber(figtype, node['ids'][0])
self.__saved_add_fignumber = self.add_fignumber
self.add_fignumber = patched_add_fignumber
self.visit_figure(node)


def depart_subfigure_html(self, node):
self.depart_figure(node)
self.add_fignumber = self.__saved_add_fignumber

def depart_subfig_html(self, node):
figoutput = "".join(self.body)
figoutput = figoutput.replace(
'class="figure',
Expand All @@ -88,7 +131,20 @@ def depart_subfig_html(self, node):
self.body.append(figoutput)


class figmatrix(nodes.General, nodes.Element):
class SubfigureDirective(Figure):
def run(self):
res_list = super().run()
if len(res_list) != 1:
return res_list

res, = res_list
# reclass nodes.figures to subfigures
if type(res) is nodes.figure:
res.__class__ = subfigure
return [res]


class figmatrix(nodes.figure):
pass


Expand Down Expand Up @@ -121,12 +177,10 @@ class FigmatrixDirective(Directive):
}

def run(self):
label = self.options.get("label", None)
width = self.options.get("width", None)
alt = self.options.get("alt", None)

ids = [label] if label is not None else []
node = figmatrix("", ids=ids)
node = figmatrix("")
if width is not None:
node["width"] = width
if alt is not None:
Expand All @@ -145,43 +199,106 @@ def run(self):
)
node += caption

label = self.options.get("label", None)
if label is not None:
targetnode = nodes.target("", "", ids=[label])
node.append(targetnode)

node['names'].append(label)
self.state.document.note_explicit_target(node, node)
return [node]


def node_id(node):
return node["ids"][0]


def doctree_read(app, doctree):
secnums = app.builder.env.toc_secnumbers
env = app.builder.env
if not hasattr(env, 'subfigures'):
env.subfigures = {}

doc_subfigures = env.subfigures.setdefault(env.docname, {})

for figmatrix_node in doctree.traverse(figmatrix):
figmatrix_id = node_id(figmatrix_node)
subfig_i = 0

i = 0
# we can't use enumerate, as we delete nodes on the go
while i < len(figmatrix_node.children):
removed_nodes = 0
figure_node = figmatrix_node.children[i]
if isinstance(figure_node, nodes.figure):
# move the figure inside a subfig node
subfig_children = [figure_node]

if isinstance(figure_node, subfigure):
if i > 0:
# if the node before the figure is a target
# move it inside the subfig
prevnode = figmatrix_node.children[i - 1]
if isinstance(prevnode, nodes.target):
figmatrix_node.children.remove(prevnode)
subfig_children.insert(0, prevnode)
figure_node.insert(0, prevnode)
removed_nodes += 1

cur_subfig = subfig("", *subfig_children)
cur_subfig["width"] = figmatrix_node["width"]
cur_subfig["subfig_i"] = subfig_i
figmatrix_node.replace(figure_node, cur_subfig)
figure_node["width"] = figmatrix_node["width"]
figure_node["subfig_i"] = subfig_i
doc_subfigures[node_id(figure_node)] = (figmatrix_id, subfig_i)
subfig_i += 1
elif isinstance(figure_node, nodes.figure):
raise ExtensionError(_("`figure' can't be used in figmatrix, use `subfigure'"))
i += 1 - removed_nodes

def env_purge_doc(app, env, docname):
if not hasattr(env, 'subfigures'):
return

env.subfigures.pop(docname, None)


def env_merge_info(app, env, docnames, other) -> None:
if not hasattr(other, 'subfigures'):
return

if not hasattr(env, 'subfigures'):
env.subfigures = {}

env.subfigures.update(other.subfigures)


class TocFixupCollector(EnvironmentCollector):
def clear_doc(self, app: Sphinx, env: BuildEnvironment, docname: str) -> None:
pass

def merge_other(self, app: Sphinx, env: BuildEnvironment, docnames: Set[str],
other: BuildEnvironment) -> None:
pass

def process_doc(self, app: Sphinx, doctree: nodes.document) -> None:
pass

def get_updated_docs(self, app: Sphinx, env: BuildEnvironment) -> List[str]:
updated_docs: Set[str] = set()

# for each document, get the figure numners
for docname, doc_fignumbers in env.toc_fignumbers.items():
updates = 0
# skip the document if there are no subfigures
doc_subfig_fignumbers = doc_fignumbers.get("subfigure", None)
if doc_subfig_fignumbers is None:
continue

doc_fig_fignumbers = doc_fignumbers.get("figure", None)
if doc_fig_fignumbers is None:
continue

doc_subfigures = env.subfigures[docname]
for subfigure_id, (figmatrix_id, subfig_i) in doc_subfigures.items():
figmatrix_fignumer = doc_fig_fignumbers[figmatrix_id]
subfig_letter = chr(ord("a") + subfig_i)
doc_subfig_fignumbers[subfigure_id] = figmatrix_fignumer + (subfig_letter,)
updates += 1

if updates:
updated_docs.add(docname)

return list(updated_docs)


def setup(app):
# add_enumerable_node registers a node class as a numfig target
Expand All @@ -194,13 +311,19 @@ def setup(app):
latex=(visit_figmatrix_tex, depart_figmatrix_tex),
)

app.add_node(
subfig,
html=(visit_subfig_html, depart_subfig_html),
singlehtml=(visit_subfig_html, depart_subfig_html),
# number subfigures in a separate space
app.add_enumerable_node(
subfigure,
"subfigure",
html=(visit_subfigure_html, depart_subfigure_html),
singlehtml=(visit_subfigure_html, depart_subfigure_html),
text=(skip_visit, None),
latex=(visit_subfig_tex, depart_subfig_tex),
latex=(visit_subfigure_tex, depart_subfigure_tex),
)

app.add_directive("figmatrix", FigmatrixDirective)
app.add_directive("subfigure", SubfigureDirective)
app.connect("doctree-read", doctree_read)
app.connect('env-purge-doc', env_purge_doc)
app.connect('env-merge-info', env_merge_info)
app.add_env_collector(TocFixupCollector)

0 comments on commit 20aa671

Please sign in to comment.