Привет, молодые успешные!

Как часто вы работаете с PDF документами? Случалось ли вам сталкиваться с проблемой монолитности этого формата? Я часто сталкивался с такой проблемой, когда мне требовалось лишь несколько листов из всего документа, например чтобы отправить их по почте. Да можно воспользоваться бесплатными онлайн сервисами которых полно в интернете...

Но если документ важен и хранит в себе коммерческую тайну, или в PDF документе отсканирован паспорт? Нет никаких гарантий что документ не попадет к злоумышленникам.

Именно эту задачу и должен решить мой проект! Это должно быть простое, быстрое решение, работающее локально на вашей машине.

Также этот проект может подойти студентам колледжей и университетов.

С исходниками проекта вы можете ознакомиться в моем git репозитории https://github.com/BakaLaver/PDFSplitter, а так же релиз https://github.com/BakaLaver/PDFSplitter/releases/tag/1.0.1.

Да там уже реализована возможность объединять документы, но это для следующих статей.

Инструменты

Как сказано в заголовке, мы будем использовать язык программирования c#, а конкретно пользовательский интерфейс WPF.

Для работы с PDF документами нам понадобиться библиотека itextsharp, обширная библиотека позволяющая работать с широким спектром электронных документов. Конкретно в этом проекте я использовал версию 5.5.13.3, выяснилось что есть версия 8.0.3 уже после того как я разобрался в версии 5.

Ну и сам факт использования WPF склоняет нас к паттерна MVVM.

Планирование

Для удобного управления проектом следует разделить ответственность на слои, бизнес-логика, презентация, и слой данных.

Бизнес-логика

На этот слой возлагается ответственность работы с pdf документами. Здесь будет проходить основная работа над документом. Так же здесь будет объявлен сервис дающий доступ к нашим моделям бизнес логики.

Слой бизнес логики будет иметь следующую структуру:

  • Папка BusinessModels, будет хранить классы основной логики для работы с pdf документами

  • Папка Services, папка промежуточных классов, они нужны для того чтобы взаимодействовать со внешними слоями для того чтобы мы могли инкапсулировать нашу основную логику

Представление

Слой будет отвечать за внешний вид нашей программы, здесь будет основа реализации паттерна MVVM, то есть здесь будет определенны папки Model, View, ViewModel.

  • Model: Здесь будут представлены сущности предоставляющие данные для слоя бизнес логики

  • View: В этой части будет определенно наше основное окно (на данный момент задумано одно окно)

  • ViewModel: Место основной логики взаимодействия между Model и View, отсюда будет прямое обращение к слою бизнес логики

Данные

Так как у нас не будет подключения к базам данных, слой данных будет представлен файловой системой нашей операционной системы Windows, где будут храниться наши файлы pdf и куда они будут сохранятся.


И так, предлагаю закончить на этом планирование и приступить уже к конкретной реализации

Реализация

Начнем с создания WPF проекта

В итоге имеем такое окно

Приведем к описанной выше структуре

Начальный порядок навели, теперь можно приступать к реализации бизнес логики

Бизнес логика. Реализация

Бизнес логика будет сосредоточена в классической библиотеке классов, поэтому без лишних слов ПКМ => Добавить => Создать проект…

Прошу заметить, что желательно располагать связанные проекты в одном месте сразу, для упрощения создания репозиториев.

После создания проекта, безотлагательно приводим его структуру к описанной в разделе планирования.

Как только мы справились с этой задачей, можно подключать самый главный инструмент данного проекта, а именно nuget пакет iTextSharp 5.5.13.3. Зависимости => ПКМ => Управление пакетами Nuget, далее переключаемся на вкладку “Обзор” и в строку поиска вводим заветное “iTextSharp”. 

Нужный нам вариант должен быть на первой строчке в результате поиска.

Подготовительные мероприятия закончены, теперь мы наконец то приступаем к программированию. Добавим класс в папку “BusinessModels”, назовем его “SplitPage” В нем определим несколько свойств.

 internal class SplitPage

 {

     private PdfReader reader { get; set; }

     private Document sourceDocument { get; set; }

     private PdfCopy pdfCopyProvider { get; set; }

     private PdfImportedPage importedPage { get; set; }

 }

О каждом подробней

  • PdfReader reader. Будет отвечать за чтение PDF документа источника, через него мы будем получать характеристики документа

  • Document sourceDocument. Сюда будет сохраняться “парсенный” документы источник, то есть документ откуда будут добываться нужный диапазон страниц

  • PdfCopy pdfCopyProvider. Делает копию PDF документа, такой объект полностью открыт для редактирования.

  • PdfImportedPage importedPage. Класс представляет собой нечто типа промежуточного состояния передаваемой страницы, когда страница уже не является частью документа источника, и еще не добавлена в новый документ.

Со свойствами разобрались, теперь нужно описать сердце нашей бизнес логики, а именно метод в котором будет происходить вся магия. Метод будет называться “ExtractPages” и будет иметь следующую сигнатуру.

public void ExtractPages(string sourcePDFpath, string outputPDFpath, int startpage, int endpage)

Метод не будет ничего возвращать и будет иметь следующие параметры

  • string sourcePDFpath. строковый параметр передающий полный путь (то есть и имя файла то же) документа источника

  • string outputPDFpath. строковый параметр передающий полный путь для нового документа содержащий в себе нужный диапазон страниц. Обратите внимание что нужно передать путь с именем конечного файла и его расширением .pdf

  • int startpage и int endpage. целочисленные параметры указывающие номера страниц, с какой и по какую нужно достать страницы.

В теле цикла будет определена следующая логика.

public void ExtractPages(string sourcePDFpath, string outputPDFpath, int startpage, int endpage)

{

    reader = new PdfReader(sourcePDFpath); //1

    sourceDocument = new  Document(reader.GetPageSizeWithRotation(startpage));//2

    pdfCopyProvider = new PdfCopy(sourceDocument, new System.IO.FileStream(outputPDFpath, System.IO.FileMode.Create));//3

 sourceDocument.Open();

    for (int i = startpage; i <= endpage; i++)

    {

        importedPage = pdfCopyProvider.GetImportedPage(reader, i); //4

        pdfCopyProvider.AddPage(importedPage);

    }

    sourceDocument.Close();

    reader.Close();

}

Теперь по порядку что здесь происходит.

  1. Здесь инициализируется объект класса PdfReader в конструктор которого передается параметр метода sourcePDFpath.

  2. В этом месте инициализируется  объект типа Document, в конструктор которого передается метод объекта класса PdfReader “reader.GetPageSizeWithRotation(startpage)”, из этого метода задается размер страниц.

  3. Тут самый сложный момент для моего понимания, по идее конструктор класса PdfCopy должен копировать переданный ему документ в первом параметре, но по факту создается пустой pdf документ (если после этой строчки что то идет не так, не забудьте очистить папку назначения, потому что при каждой неудачной попытке будет создаваться пустой документ). Так или иначе, тут в конструкторе первым параметром передается объект документа источника, вторым инициализируеться файловый поток создающий файл по заданному пути (параметр outputPDFpath).

  4. И наконец то мы начинаем процесс переноса заданного диапазона страниц, он начинаются с открытия документа с помощью метода Open(), после этого запускается цикл for где начальная точка это первая заданная страница, а последняя…последняя. Во время каждой итерации в свойство importedPage будет передаваться страница из документа источника через метод GetImportedPage, куда передается объект класса PdfReader, а также индекс нужной страницы. После чего полученная страница добавляется в новый pdf документ. Когда все страницы будут добавлены в новый документ, все использованные документы следует закрыть, а задачу по вытягиванию страниц из pdf документа можно считать выполненной.

Теперь. когда основная логика готова, нужно обеспечить вызов нашей логики внешними сборками, для этого создадим класс в папке “Service” и назовем его “PDFService”.

private SplitPage _SplitPageCommand;

public PDFService() 

{

    _SplitPageCommand = new SplitPage();//1

}

public void ExtractPageFromTo(string sourcePDFpath, string outputPDFpath, int startpage, int endpage) 

{

    _SplitPageCommand.ExtractPages(sourcePDFpath, outputPDFpath, startpage, endpage);//2

}

Как обеспечивается доступ к нашей логике.

  1. В начале объявим закрытое свойство класса нашей логики “SplitPage” это обеспечит доступ к нему из всего текущего класса

  2. Проинициализируем наше свойство в конструкторе класса, это гаранируем что наше свойство никогда не будет null

  3. Метод ExtractPageFromTo дублирует сигнатуру метода логики класса “SplitPage” и просто вызывает соответствующий метод, а также название метода информирует о том что делает этот метод.

Такой способ построения кажется мне самым удобным, он обеспечивает расширяемость, если я захочу расширить свой проект новыми свойствами, то такое построение проекта обеспечит мне это.

Бизнес логика. Тестирование

Чтобы убедиться в работоспособности нашего решения здесь и сейчас, до реализации пользовательского интерфейса, мы можем создать проект unit тест и обратиться к нашей логике оттуда.

По решению “PDFSlicer” ПКМ => Добавить => Новый проект… Здесь нам нужно выбрать “Тестовый проект xUnit”

Далее называем как нам удобно, я обычно называю его именем решения которое он будет тестировать, приписывая в конце .Test

После того как тестовый проект будет добавлен, следует добавить в зависимости ссылку на тестируемую библиотеку, ПКМ => Зависимости => Добавить ссылку на проект, после чего выбрать нашу библиотеку бизнес логики.

Теперь можно приступить к самому тестированию, в тестовом проекте по умолчанию  реализован класс “UnitTest1”, с методом внутри “Test1()”, для наших целей этого вполне достаточно, для теста возьмем тестовый многостраничный PDF документ взятый из интернета по запросу “test pdf” (https://axiomabio.com/pdf/test.pdf), в теле метода напишем следующий код.

public class UnitTest1

{

    [Fact]

    public void Test1()

    {

	PDFService split = new PDFService();

        split.ExtractPageFromTo(@"D:\\TestPDF\test.pdf", @"D:\\TestPDF\test1.pdf", 2,5);

    }

}

И выполним тест ПКМ по коду => Выполнить тест, в результате видим следующее окно.

 

В результате видим по указанному пути два файла, один создан нашим методом, другой изначальный.

Проверим количество страниц, мы видим что в документе источнике 10 страниц.

А в нашем производном от теста файле 4 страницы, что соответствует введенным нами параметрам, от второй страницы по пятую.

Также в результате теста выяснилось несколько вещей.

  • Если файл с изъятыми страницами сохраняется в ту же папку где хранится исходный документ,  следует проследить что имя производного файла не совпадает с исходным файлам (приводит к исключению).

  • Также указывая нужный диапазон страниц следует иметь ввиду реальные индексы страниц. Условно говоря если в документе 10 страниц, а вы укажите диапазон до 11 страницы, то мы получим исключение.

С бизнес логикой мы наконец закончили, теперь можно приступать к реализации пользовательского интерфейса.

Представление. Реализация

Прежде чем реализовывать паттерн MVVM, нужно примерно спланировать как будет выглядеть наш интерфейс. Нарисуем примерный макет (я пользуюсь draw.io). Я не дизайнер заранее извиняюсь.

На основе нашего макета мы также сможем прикинуть примерную схему нашей привязки.

Здесь от 1 по 5 пункт, представлены свойства модели, 6 пункт представляет привязку кнопки к свойству команды, нажатие по кнопке вызывает обращение к методам нашей логики извлечения документа.

Представление. Модель

Начнем мы с реализации класса модели. Модель будет представлена классом “SplitPDFFromTo” и реализовывать интерфейс “INotifyPropertyChanged”, он поможет нам реализовать полноценную привязку, и благодаря ему мы можем оповещать нашу ViewModel о изменениях. Все свойства используют полноценную реализацию (приватное поле, публичное свойство), для того чтобы иметь возможность вызывать методы “INotifyPropertyChanged”.

public class SplitPDFFromTo : INotifyPropertyChanged

{

    private string _inPutPath;

    private string _outPutPath;

    private string _newDocumentName;

    private int _from;

    private int _to;

    public int From 

    {

        get { return _from; }

        set 

        { 

            _from = value;

            OnPropertyChanged("From");

        }

    }

    public int To 

    {

        get { return _to; }

        set 

        {

            _to = value;

            OnPropertyChanged("To");

        }

    }

    public string NewDocumentName 

    {

        get { return _newDocumentName; } 

        set 

        {

            _newDocumentName = value;

            OnPropertyChanged("NewDocumentName");

        }

    }

    public string InPutPath 

    {

        get { return _inPutPath; } 

        set 

        {

            _inPutPath = value;

            OnPropertyChanged("InPutPath");

        }

    }

    public string OutPutPath 

    {

        get { return _outPutPath; }

        set 

        {

            _outPutPath = value;

            OnPropertyChanged("OutPutPath");

        }

    }

    public event PropertyChangedEventHandler PropertyChanged;

    public void OnPropertyChanged([CallerMemberName] string prop = "")

    {

        if (PropertyChanged != null)

            PropertyChanged(this, new PropertyChangedEventArgs(prop));

    }

}

С моделью закончили, можно переходить к реализации VIewModel.

Представление.ViewModel

Пока что у нас только один View это основное окно MainWindow, для него мы и создадим наш ViewModel, он будет представлен классом “MainWindowsModel”.

Прежде чем начать работать над “ViewModel” мы должны дать ссылку на наш проект бизнес логики для нашей презентации, “Зависимости => ПКМ => Добавить ссылку на проект…” и выбираем наш проект бизнес логики.

В начале мы объявляем основные свойства ViewModel.

 public class MainWindowsModel 

 {

     private PDFService TakePagesService { get; set; }

     private SplitPDFFromTo FromToModel {  get; set; }

     public MainWindowsModel() 

     {

         FromToModel = new SplitPDFFromTo();

        TakePagesService = new PDFService();

     }

     private RelayCommand _selectFromToInFileCommand;

     private RelayCommand _selectFromToOutFileCommand;

     private RelayCommand _takePagesFromToCommand;

}

Здесь объявляется свойства классов PDFService и SplitPDFFromTo, а так же идет их инициализация в конструкторе класса, тем самым методы класса могут обращаться к свойствам из любой части кода, а когда создается объект класса конструктор гарантирует нам что эти свойства не будут “null”.

Также у нас объявлены поля типа “RelayCommand” для взаимодействия с событиями в нашем View (нажатие на кнопку и тд.). Сам класс реализован в классическом виде (и взят отсюда https://metanit.com/sharp/wpf/22.3.php).

public class RelayCommand : ICommand

{

    private Action<object> execute;

    private Func<object, bool> canExecute;

    public event EventHandler CanExecuteChanged

    {

        add { CommandManager.RequerySuggested += value; }

        remove { CommandManager.RequerySuggested -= value; }

    }

    public RelayCommand(Action<object> execute, Func<object, bool> canExecute = null)

    {

        this.execute = execute;

        this.canExecute = canExecute;

    }

    public bool CanExecute(object parameter)

    {

        return this.canExecute == null || this.canExecute(parameter);

    }

    public void Execute(object parameter)

    {

        this.execute(parameter);

    }

}

Для хранения создадим ему папку “Command” внутри папки “ViewModel”.

Для полей “RelayCommand” определенные свойства которые будут реагировать на событие внутри View, в нашем случае это, выбор файла источника, выбор папки сохранения, и запуск процесса извлечения страниц.

О всех этих событиях подробно далее.

Выбор файла источника.

  public RelayCommand SelectFromToInFileCommand

 {

     get

     {

         return _selectFromToInFileCommand ??

           (_selectFromToInFileCommand = new RelayCommand(obj =>

           {

               FromToModel.InPutPath = SelectSourceFile();

           }));

     }

 }

private string SelectSourceFile()

{

    string path = "" ;

    FileSelection.OpenFileDialog op = new FileSelection.OpenFileDialog();

    op.Filter = "PDFfile|*.pdf";

    op.DefaultExt = "pdf";

    if (op.ShowDialog() == true)

    {

        path = op.FileName;

    }

    return path;

}

Здесь в свойстве  “SelectFromToInFileCommand” идет реакция на нажатие кнопки которая вызывает метод “SelectSourceFile” открывающий окно выбора файла исходника. В результате работы метода идет возврат строки пути выбранного файла, передающийся в свойство модели “FromToModel.InPutPath”.

Выбор папки сохранения

public RelayCommand SelectFromToOutFileCommand

 {

     get

     {

         return _selectFromToOutFileCommand ??

           (_selectFromToOutFileCommand = new RelayCommand(obj =>

           {

               FromToModel.OutPutPath = SelectOutFlder();

           }));

     }

 }

private string SelectOutFlder() 

{

    string path = "";

    var dialog = new CommonOpenFileDialog();

    dialog.IsFolderPicker = true;

    CommonFileDialogResult result = dialog.ShowDialog();

    if (result == CommonFileDialogResult.Ok) 

    {

        path = dialog.FileName;

    }

    return path;

}

Логика вызова команды “SelectFromToOutFileCommand” схожа с вызовом “SelectFromToInFileCommand” за тем исключением что здесь вызывается метод “SelectOutFlder” открывающий диалоговое окно для выбора папки, и результатом работы метода будет строка пути к папке назначения, которая передается в свойство модели  “FromToModel.OutPutPath”.

В методе “SelectOutFlder” используются компоненты nuget пакета “WindowsAPICodePack-Shell”

Запуск процесса извлечения страниц

public RelayCommand TakePagesFromToCommand

{

    get

    {

        return _takePagesFromToCommand ??

          (_takePagesFromToCommand = new RelayCommand(obj =>

          {

              FromToCall();

              var fullPath = FromToModel.OutPutPath + @"\" + FromToModel.NewDocumentName + ".pdf";

              OpenFolerQuestion(fullPath);

          }));

    }

}

private void FromToCall()

{

    string outPath = FromToModel.OutPutPath + @"\" + FromToModel.NewDocumentName + ".pdf";

    TakePagesService.ExtractPageFromTo(FromToModel.InPutPath, outPath, FromToModel.From, FromToModel.To);

}

private void OpenFolerQuestion(string path) 

{

    var dialogResult = MessageBox.Show("Открыть папку с документом?", "Готово!", MessageBoxButton.YesNo);

    string fullPath = path;

    if (dialogResult == MessageBoxResult.Yes)

    {

        fullPath = System.IO.Path.GetFullPath(fullPath);

        System.Diagnostics.Process.Start("explorer.exe", string.Format("/select,\"{0}\"", fullPath));

    }

}

Эта команда отвечает за финальный этап пользовательского опыта, когда все поля заполнены и требуется получить результат. В блоке get свойства “TakePagesFromToCommand” первым делом вызывается метод “FromToCall”, в теле метода в строковой переменной “outPath” собирается полный путь нового документа, из полей модели “OutPutPath” и “NewDocumentName” (не забываем дополнить путь расширением файла “.pdf”), в этот документ  будет сохраняться изъятые страницы.

Далее вызывается основная логика через свойство “TakePagesService” метод ExtractPageFromTo, куда мы передаем данные полученные от нашего View.

После того как метод “FromToCall” выполнил свою работу, мы возвращаемся к нашей команде, где я решил ввести инструмент демонстрации выполненной работы нашей бизнес логики, а именно предложить открыть расположение нового файла, чтобы иметь возможность сразу проверить результат.

За это отвечает метод “OpenFolerQuestion”, вызов которого вызывает окно с вопросом, нужно ли открыть папку с новым документом.

Это все что связанно с нашей ViewModel, теперь я предлагаю наконец вспомнить о пользователе и заняться интерфейсом в нашем View.

Представление.View

Сразу предупрежу, я не большой спец в верстке или разметке страницы, поэтому я заострю внимание на основных аспектах окна. Также я не буду указывать код целиком, он однотипный и его много, с полной версией вы можете ознакомиться в моем git репозитории.

В начале я разделяю страницу по горизонтали, в верхней части будет реализован интерфейс для разделения pdf документов, а нижнюю я оставлю для будущих статей.

<Grid>

     <Grid.RowDefinitions>

        <RowDefinition Height="*"/>

        <RowDefinition Height="*"/>

    </Grid.RowDefinitions>

    <Grid Grid.Row="0">

        <Grid.ColumnDefinitions>

            <ColumnDefinition></ColumnDefinition>

        </Grid.ColumnDefinitions>

        <Grid.RowDefinitions>

            <RowDefinition Height="*"/>

        </Grid.RowDefinitions>

        <StackPanel x:Name="SplitPageDropFile" AllowDrop="True" Drop="SplitPageDropFile_Drop" Background="Azure">

            <Label Grid.Row="0" Grid.Column="0" FontWeight="Bold" Content="Вытащить страницы (страницу)" VerticalAlignment="Center" HorizontalAlignment="Center"/>

        </StackPanel>

    </Grid>

    <StackPanel Grid.Row="1" Background="Lavender">

        <Label Grid.Row="0" Grid.Column="1" FontWeight="Bold" Content="Объединение документов(в разработке)" VerticalAlignment="Center" HorizontalAlignment="Center"/>

    </StackPanel>

</Grid>

Здесь код представлен без блока Window внутри которого он должен быть. 

Начнем с разметки наших полей, под каждую строку мы выделяем свой блок. Так же все блоки будут находиться внутри блока “StackPanel”, он нужен для того чтобы реализовать функцию “drag and drop file” для этого указывается параметр “ AllowDrop="True" “  это должно облегчить жизнь пользователю. 

Для взаимодействия нашего “View” и “ViewModel” используем обычный “Binding” внутри параметров тэгов, здесь привязка будет применяться либо в тэге “TextBox”, либо в “Button” возьмем по одному из них для примера.

<TextBox Grid.Row="0" IsEnabled="False" Grid.Column="2" Text="{Binding FromToModel.InPutPath, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>

Здесь идет привязка к свойству пути к исходному файлу в нашем “Model”, мы получаем к нему доступ через свойство “FromToModel” в нашем “ViewModel” тем самым “View” нечего не знает о “Model”, также мы указываем параметр “Mode=TwoWay” что указывает на двухстороннее движение данных как от “View” к “Model”, так и наоборот, и “UpdateSourceTrigger=PropertyChanged” этот параметр будет требовать обновления реагировать на изменения свойства. 

Да, и я отключил прямой ввод пути, путь можно ввести только через функцию “drag and drop file”, либо выбрав файл в диалоговом окне. Пользователь без выбора, хороший пользователь.

<Button Content="..." Command="{Binding SelectFromToInFileCommand}" Grid.Row="0" Grid.Column="3"/>

У кнопки мы привязываем свойство команды к нашей к…нашей команде в нашем “ViewModel” и в принципе всё.

Теперь осталось финишная прямая, задать логику окна в “.cs” файле.

private MainWindowsModel ViewModel { get; set; }

public MainWindow()

{

    InitializeComponent();

    ViewModel = new MainWindowsModel();

    DataContext = ViewModel;

}

Здесь мы объявляем приватное свойство типа “MainWindowsModel” нашего “ViewModel”. Инициализация новым объектом “MainWindowsModel” проводиться в конструкторе класса, сразу после инициализации компонентов окна, после чего ссылка на объект передается в “DataContext” что связывает “View” и “ViewModel”.

private void SplitPageDropFile_Drop(object sender, DragEventArgs e)

{

    if (e.Data.GetDataPresent(DataFormats.FileDrop)) 

    {

        string[] file = (string[])e.Data.GetData(DataFormats.FileDrop);

        if (CheckDropedFile(file[0]))

        {

            ViewModel.FromToModel.InPutPath = file[0];

        }

        else 

        {

            MessageBox.Show("Допустимы только PDF файлы");

        }

    }

}

private bool CheckDropedFile(string path)

{

    bool result = false;

    FileInfo fileInf = new FileInfo(path);

    if (fileInf.Extension == ".pdf")

    {

        result = true;

    }

    return result;

}

В событии “SplitPageDropFile_Drop” элемента “StackPanel” идет проверка “дропнутого” файла, сама проверка проходит в приватном методе “CheckDropedFile” куда передается путь к файлу, сам метод возвращает “true” или “false” исходя из результата проверки.

Финальная проверка

Теперь когда все находиться на своих местах мы готовы проверить наше решение в действии.

В начале нужно указать путь к нашему файлу, за основу возьмем тестовый файл из блока тестирования бизнес логики.

Также напоминаю что вы можете просто перетащить нужный вам файл в эту область.

Далее вводим название нового документа, куда попадут наши изымаемые страницы, и место его сохранения, в моем случае это та же папке где хранится документ источник.

Теперь указываем нужный нам диапазон, от какой страницы и до какой нужно изъят страницы. Не забываем о важности реальной нумерации страниц, если указать страницу которой нет в документе, программе станет плохо.

После того как мы все ввели мы готовы начать извлечение, для этого нажмем на кнопку “Извлечь”.

По завершению операции мы получим сообщение о готовности, и предложение открыть папку с новым документом.

Как видим файл действительно там где нам и нужно, и страниц в нем сколько мы и просили(страницы изымаются включительно с теми которые были указаны).

Итог

В ходе проекта мы применили паттерн MVVM, а так же разделили структуру проекта на слои, что обеспечило легкую тестируемость нашего кода.

В итоге мы получили легкий инструмент для такой редкой задачи.

Спасибо что дочитали до сюда, проект еще не закончен в нем еще много чего можно сделать, я также буду рад вашим отзывам и предложениям по фичам.

Комментарии (29)


  1. FuzzyWorm
    18.05.2024 09:48
    +9

    Распечатать выборочные страницы из PDF документа на виртуальный PDF принтер не проще ли?


    1. RichardMerlock
      18.05.2024 09:48
      +2

      Это вообще самый оптимальный и удобный вариант с сохранением в ПДФ нужных страниц и он зачастую уже есть в системе. Но тут похоже была жажда программирования. Гораздо полезнее было сделать ПДФ-собиралку вместо разрезалки. Вот чтобы из микса картинок и других ПДФ-ок собрать одну.


      1. PereslavlFoto
        18.05.2024 09:48
        +1

        Для этого есть LaTeX !

        :-)


        1. RichardMerlock
          18.05.2024 09:48
          +2

          Пока нет. Надо статью писать, чтобы было.


      1. BakaLoverCode Автор
        18.05.2024 09:48

        В моём гите уже есть реализация собиралки из разных PDF, в рамках этого проекта (в другой статье опишу).


    1. BakaLoverCode Автор
      18.05.2024 09:48

      Идея была в том чтобы вынести эту функцию удобный интерфейс (идею подал пользователь с работы)


  1. RockoPopo
    18.05.2024 09:48

    Офигеть!

    Учитесь как делать слона из праха мухи ))

    Уже лет десять и разделяю и вырезаю и обьекты удаляю из  PDF документов, и рекламу из журналов удаляю.

    С помощью Adobe Acrobat Pro 9.0.0  андэр зе макось. Наверное уже и новее есть версии, но нет нужды менять. )

    А тут портянка текста и кода на неделю, без охлаждения мозга )))


    1. PereslavlFoto
      18.05.2024 09:48

      Разве Acrobat Pro можно получить без оплаты?


      1. RockoPopo
        18.05.2024 09:48

        Вы можете использовать любой вариант, с оплатой или без.


        1. PereslavlFoto
          18.05.2024 09:48

          Без оплаты лицензию не дают!


          1. RockoPopo
            18.05.2024 09:48

            Её и за деньги не дают, дают серийный номер.


  1. Kahelman
    18.05.2024 09:48
    +2

    Вообще-то куча консольных утилит которые делают тоже самое и ещё больше.

    Быстрый поиск по pdf Tool cli:

    https://github.com/uroesch/pdftools

    Пользуйтесь.


  1. sekuzmin
    18.05.2024 09:48
    +2

    Отличный велосипед. Для поклонников консоли могу отрекомендовать pdftk-java

    https://gitlab.com/pdftk-java/pdftk

    There are pdftk-java packages available in a few repositories, including Arch, Debian / Ubuntu, Fedora / EPEL (for CentOS, RHEL, Rocky), Gentoo, Homebrew, MacPorts, Mageia, and SUSE.


    1. Kahelman
      18.05.2024 09:48

      Им пользуюсь/пользовался, не вспомнил сразу а искать было день :)


  1. Danismind
    18.05.2024 09:48
    +2

    Добрый день, больше похоже на дипломную работу, не хватает только экономической части.


    1. BakaLoverCode Автор
      18.05.2024 09:48

      Да я то же так подумал, но по другому писать не умею


    1. baldr
      18.05.2024 09:48

      не хватает только экономической части.

      Ещё часть про охрану труда. У нас была важнее всего остального диплома, судя по всему..


    1. Kahelman
      18.05.2024 09:48

      И ещё главы про экологию

      И безопасность труда :)


  1. miksoft
    18.05.2024 09:48

    А раз зашла речь про разделение pdf, то подскажите, пожалуйста, инструмент для выделения из pdf прямоугольной области. Есть в pdf план помещения на нескольких страницах, на каждом листе изображены разные свойства помещения - размеры, проводка, мебель и т.д. Нужно как-то выделить прямоугольную область и по ней на всех листах обрезать содержимое и увеличить до стандартного листа A4. Чтобы в итоге напечатать или передавать контрагентам информацию о фрагменте помещения и, одновременно, иметь нормальный масштаб при печати.


    1. BakaLoverCode Автор
      18.05.2024 09:48
      +1

      Интересная идея, я заскриню и на досуге подумаю


    1. zamboga
      18.05.2024 09:48
      +1

      Я использую обычный скриншотер для этого. Увеличиваю максимально на площадь экрана PDF, и делаю скрин области, потом его и печатаю или отправляю картинкой.

      Для пакетной обработки использую любой pdf2png, а потом также оптом через графический редактор обрезаю все файлы до нужной области.


      1. RichardMerlock
        18.05.2024 09:48
        +1

        Бывает, что в экран влезает очень мелко. Можно лист pdf в любом приличном редакторе открыть c dpi на сколько оперативки хватит, а потом взять что требуется.


      1. miksoft
        18.05.2024 09:48

        Пока я примерно так и выкручиваюсь. В XnView MP поставил аддон для чтения pdf. Но пришлось задать конвертацию в растр с разрешением 600 dpi, чтобы мелкие цифры не превращались в три мутных пикселя. А с таким большим растром он заметно тормозит.


    1. Paulus
      18.05.2024 09:48
      +1

      Уже упомянутый драйвер печати в PDF умеет не только делить документы на страницы, но и страницы на части. Чтоб, например, разделить страницу на две волне достаточно. Если ещё и с полями поиграться, то можно разбить страницу на несколько документов, один и будет нужным прямоугольником.

      Из преимуществ в результате получаем настоящий PDF, а не картинку. Плюс можно наглядно объяснить шефу, как можно заработать больше денег если не жаться на лицензию для нормального продукта :)


      1. miksoft
        18.05.2024 09:48

        А можно назвать конкретное название драйвера для печати в PDF?

        В штатном в Windows 11 Microsoft Print to PDF можно только указать размер прямоугольника (через масштаб), но не его расположение - он всегда в центре.


    1. falconandy
      18.05.2024 09:48

      Может как-то поможет pdfcpu - в частности, там есть команда crop и еще много других.


    1. PbIXTOP
      18.05.2024 09:48

      Просто копирую оригинальное изображение из PDF (SumatraPDF так умеет).

      Ну или извлекаю каким-нибудь xpdf-tool или poppler


  1. coodi
    18.05.2024 09:48
    +1

    Есть же pdf24. Уже лет 15 или больше


  1. RudeWalt
    18.05.2024 09:48

    Pdfsam мержит сплитит и много чего ещё, пользуйтесь