Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tool to split, merge and rearrange pages of notebooks #14

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "vendor/CLI11"]
path = vendor/CLI11
url = https://github.com/CLIUtils/CLI11
29 changes: 29 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,26 @@ endif()

# Dependencies ################################################################
#

# Submodules
find_package(Git QUIET)

if(NOT GIT_FOUND)
message(FATAL_ERROR "Cannot update submodules dependencies: "
"Git is not installed.")
endif()

message(STATUS "Updating submodules")
execute_process(COMMAND ${GIT_EXECUTABLE} submodule update --init --recursive
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
RESULT_VARIABLE GIT_SUBMOD_RESULT)

if(NOT GIT_SUBMOD_RESULT EQUAL "0")
message(FATAL_ERROR "Error while updating submodules dependencies: "
"failed with ${GIT_SUBMOD_RESULT}.")
endif()

# PNGWriter
if(Rmlab_USE_PNG STREQUAL AUTO)
find_package(PNGwriter 0.7.0 CONFIG)
elseif(Rmlab_USE_PNG)
Expand All @@ -61,6 +81,8 @@ if(PNGwriter_FOUND)
set(Rmlab_HAVE_PNG TRUE)
endif()

# CLI11
add_subdirectory(vendor/CLI11)

# Targets #####################################################################
#
Expand Down Expand Up @@ -121,6 +143,13 @@ set_property(TARGET dump PROPERTY CXX_STANDARD 11)
target_link_libraries(dump PRIVATE Rmlab)
list(APPEND Rmlab_EXTRA_TARGETS dump)

add_executable(arrange
include/rmlab/tool/arrange.cpp
)
set_property(TARGET arrange PROPERTY CXX_STANDARD 11)
target_link_libraries(arrange PRIVATE Rmlab CLI11)
list(APPEND Rmlab_EXTRA_TARGETS arrange)

# Generate Files with Configuration Options ###################################
#
configure_file(
Expand Down
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,25 @@ lines2svg share/rmlab/examples/e09e6bd4-3647-41e7-98be-b9c3b53d80c8
# creates files "test-0.svg", "test-1.svg", ... in the current directory
```

### `arrange`: split, merge and rearrange pages of notebooks

The `arrange` tool enables you to split and merge notebooks as well as to reorder their pages. Using a syntax loosely inspired from [pdftk](https://linux.die.net/man/1/pdftk), you can specify which pages to take from several notebooks, and concatenate them into a resulting, new notebook.

```bash
# Merge notebooks A and B into C
arrange A - B -o C

# Reverse the order of pages in notebook A
arrange A last-first

# Split notebook A into B and C
arrange A first-10 -o B
arrange A 11-last -o C

# Remove pages 3 and 8 from notebook A
arrange A first-2 4-7 9-last
```

## Usage API

Set environment hints:
Expand Down
309 changes: 309 additions & 0 deletions include/rmlab/tool/arrange.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,309 @@
/* Copyright 2017-2018 Axel Huebl, Matteo Delabre
*
* This file is part of lines-are-beautiful.
*
* lines-are-beautiful is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* lines-are-beautiful is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with lines-are-beautiful.
* If not, see <http://www.gnu.org/licenses/>.
*/

#include <rmlab/rmlab.hpp>
#include <CLI/CLI.hpp>

#include <vector>
#include <string>
#include <iostream>
#include <cmath>


/**
* Expect to find an exact token in an input stream starting from the
* current position. If found, advance the cursor after the token and
* return true. Otherwise, leave the cursor unchanged and return false.
*
* @param in Input stream which should contain the token.
* @param token Exact token to find in the stream.
* @return True if and only if the token was found at the current position.
*/
bool
parse_token(
std::istream & in,
const std::string& token
)
{
std::size_t i;

for(
i = 0u;
i < token.size() && in && in.peek() == token[i];
++i, in.ignore()
)
{}

if( i != token.size() )
{
// If the token was not completely found, backtrack the cursor
// at the position where it was at the start
for( ; i != 0; --i )
{
in.unget();
}

return false;
}
else
{
return true;
}
}

/**
* Parse a page number from a stream.
*
* @param in Input stream from which to extract a page number.
* @param last Index of the last page in the notebook.
* @throws std::invalid_argument If the stream does not contain a valid page number.
* @return Parsed page number.
*/
std::size_t
parse_page_number(
std::istream & in,
std::size_t last
)
{
// We read a signed number from the input stream to handle the case where
// the user types a negative page number
int page;

// Try to read a number at first, then, if it fails, check for special
// page tokens
in >> page;

if( in.bad() )
{
if( in.eof() )
{
throw std::invalid_argument{
"Unexpected end of input while parsing range."
};
}

throw std::invalid_argument{ "Unable to read from input." };
}

if( in.fail() )
{
in.clear();

if( parse_token( in, "first" ) )
{
return 1u;
}

if( parse_token( in, "last" ) )
{
return last;
}

throw std::invalid_argument{
"Expected a page number: either a number, \"first\" or \"last\"."
};
}

if( page < 1 || page > static_cast< int >( last ) )
{
throw std::invalid_argument{
"Page numbers must be between 1 and " + std::to_string( last )
};
}

return static_cast< std::size_t >( page );
}

/**
* Parse a page range from a stream.
*
* @param in Input stream from which to extract a page range.
* @param last Index of the last page in the notebook.
* @throws std::invalid_argument If the stream does not contain a valid range.
* @return Parsed page range.
*/
std::pair< std::size_t, std::size_t >
parse_page_range(
std::istream & in,
std::size_t last
)
{
std::size_t start = parse_page_number( in, last );

if( !parse_token( in, "-" ) )
{
throw std::invalid_argument{
"Expected a dash between the two page numbers in a range."
};
}

std::size_t end = parse_page_number( in, last );
return std::make_pair( start, end );
}

int
main(
int argc,
char * argv[]
)
{
CLI::App arrange{"Subset, merge and rearrange pages of notebooks."};

std::vector< std::string > inputs;
std::string output = "-";

arrange.add_option(
"-i,--input,input", inputs,
"Input notebooks and page ranges (see below)" )
->type_name( "PATH|RANGE" )
->required();

arrange.add_option(
"-o,--output", output,
"Where to create the resulting notebook", true )
->type_name( "PATH" )
->expected( 1 );

arrange.footer( R"HELP(
Input notebooks must specified using a path to the file, optionally followed by
a list of page ranges that describe which subset of pages should be included in
the final result. If no page range is specified for a notebook, all its pages
are included. Each input notebook must be separated from the others using a
single dash.

Page ranges are made up of a start page, a dash, and an end page. They must not
include whitespace. Special pages `first` and `last` refer respectively to the
first and last page of their notebook. If the start page is greater than the
end page, the order is reversed.

If the output path is not specified or is '-', the last input notebook is
overwritten with the resulting notebook.

Examples:
- merge notebooks A and B into C
arrange A - B -o C

- reverse the order of pages in notebook A
arrange A last-first

- split notebook A into B and C
arrange A first-10 -o B
arrange A 11-last -o C

- remove pages 3 and 8 from notebook A
arrange A first-2 4-7 9-last)HELP" );

CLI11_PARSE( arrange, argc, argv );

rmlab::Notebook result;

auto it = inputs.cbegin();
bool input_has_ranges = false;
rmlab::Notebook input_notebook{ *it };
std::list< rmlab::Page > ranges_result;

while( it != inputs.cend() )
{
++it;

if( it == inputs.cend() || *it == "-" )
{
// When we reach either the end of the input list or a separator
// between two notebooks, copy the last result into the global
// result. The last result is the concatenation of all ranges
// that were specified in the last notebook, or is the whole
// notebook if no such ranges were given.
if( input_has_ranges )
{
std::copy(
ranges_result.cbegin(),
ranges_result.cend(),
std::back_inserter( result.pages )
);
}
else
{
std::copy(
input_notebook.pages.cbegin(),
input_notebook.pages.cend(),
std::back_inserter( result.pages )
);
}

// If we reached a separator and there exists a notebook following
// it, load this new notebook and start over
if( it != inputs.cend() && ++it != inputs.cend() )
{
input_has_ranges = false;
input_notebook = rmlab::Notebook{ *it };
ranges_result.clear();
continue;
}
}

if( it != inputs.cend() )
{
// Read a range and concatenate it into the current result
std::istringstream range_input{ *it };
std::size_t last_page = input_notebook.npages;
auto range_indices = parse_page_range( range_input, last_page );

if( range_indices.first < range_indices.second )
{
std::copy(
std::next(
input_notebook.pages.cbegin(),
range_indices.first - 1
),
std::next(
input_notebook.pages.cbegin(),
range_indices.second
),
std::back_inserter( ranges_result )
);
}
else if( range_indices.first > range_indices.second )
{
std::copy(
std::next(
input_notebook.pages.crbegin(),
last_page - range_indices.first
),
std::next(
input_notebook.pages.crbegin(),
last_page - range_indices.second + 1
),
std::back_inserter( ranges_result )
);
}

input_has_ranges = true;
}
}

// Write the resulting notebook to the appropriate output
if( output == "-" )
{
output = input_notebook.filename;
}

result.npages = result.pages.size();
result.save( output );
return 0;
}
1 change: 1 addition & 0 deletions vendor/CLI11
Submodule CLI11 added at 92b8f6