origami-2.0.0/0000755000004100000410000000000012757133667013211 5ustar www-datawww-dataorigami-2.0.0/bin/0000755000004100000410000000000012757133666013760 5ustar www-datawww-dataorigami-2.0.0/bin/gui/0000755000004100000410000000000012757133666014544 5ustar www-datawww-dataorigami-2.0.0/bin/gui/gtkhex.rb0000644000004100000410000012433512757133666016373 0ustar www-datawww-data=begin This file is part of PDF Walker, a graphical PDF file browser Copyright (C) 2016 Guillaume Delugré. PDF Walker 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. PDF Walker 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 PDF Walker. If not, see . This work has been derived from the GHex project. Thanks to them. Original implementation: Jaka Mocnik =end require 'gtk2' module Gtk class HexEditor < Fixed module View HEX = 1 ASCII = 2 end module Group BYTE = 1 WORD = 2 LONG = 4 end class Highlight attr_accessor :start, :end attr_accessor :start_line, :end_line attr_accessor :style attr_accessor :min_select attr_accessor :valid end class AutoHighlight attr_accessor :search_view attr_accessor :search_string attr_accessor :search_len attr_accessor :color attr_accessor :view_min attr_accessor :view_max attr_accessor :highlights end DEFAULT_FONT = "Monospace 12" DEFAULT_CPL = 32 DEFAULT_LINES = 16 DISPLAY_BORDER = 4 SCROLL_TIMEOUT = 100 type_register @@primary = Clipboard.get(Gdk::Selection::PRIMARY) @@clipboard = Clipboard.get(Gdk::Selection::CLIPBOARD) def initialize(data = '') super() @data = data.force_encoding('binary') @scroll_timeout = -1 @disp_buffer = "" @starting_offset = 0 @xdisp_width = @adisp_width = 200 @xdisp_gc = @adisp_gc = nil @active_view = View::HEX @group_type = Group::BYTE @lines = @vis_lines = @top_line = @cpl = 0 @cursor_pos = 0 @lower_nibble = false @cursor_shown = false @button = 0 @insert = false @selecting = false @selection = Highlight.new @selection.start = @selection.end = 0 @selection.style = nil @selection.min_select = 1 @selection.valid = false @highlights = [ @selection ] @auto_highlight = nil @disp_font_metrics = load_font DEFAULT_FONT @font_desc = Pango::FontDescription.new DEFAULT_FONT @char_width = get_max_char_width(@disp_font_metrics) @char_height = Pango.pixels(@disp_font_metrics.ascent) + Pango.pixels(@disp_font_metrics.descent) + 2 @show_offsets = false @offsets_gc = nil self.can_focus = true self.events = Gdk::Event::KEY_PRESS_MASK self.border_width = DISPLAY_BORDER mouse_handler = lambda do |widget, event| if event.event_type == Gdk::Event::BUTTON_RELEASE and event.button == 1 if @scroll_timeout GLib::Source.remove @scroll_timeout @scroll_timeout = nil @scroll_dir = 0 end @selecting = false Gtk.grab_remove(widget) @button = 0 elsif event.event_type == Gdk::Event::BUTTON_PRESS and event.button == 1 self.grab_focus unless self.has_focus? Gtk.grab_add(widget) @button = event.button focus_view = (widget == @xdisp) ? View::HEX : View::ASCII if @active_view == focus_view if @active_view == View::HEX hex_to_pointer(event.x, event.y) else ascii_to_pointer(event.x, event.y) end unless @selecting @selecting = true set_selection(@cursor_pos, @cursor_pos) end else hide_cursor @active_view = focus_view show_cursor end elsif event.event_type == Gdk::Event::BUTTON_PRESS and event.button == 2 # TODO else @button = 0 end end @xdisp = DrawingArea.new @xdisp.modify_font @font_desc @xlayout = @xdisp.create_pango_layout('') @xdisp.events = Gdk::Event::EXPOSURE_MASK | Gdk::Event::BUTTON_PRESS_MASK | Gdk::Event::BUTTON_RELEASE_MASK | Gdk::Event::BUTTON_MOTION_MASK | Gdk::Event::SCROLL_MASK @xdisp.signal_connect 'realize' do @xdisp_gc = Gdk::GC.new(@xdisp.window) @xdisp_gc.set_exposures(true) end @xdisp.signal_connect 'expose_event' do |xdisp, event| imin = (event.area.y / @char_height).to_i imax = ((event.area.y + event.area.height) / @char_height).to_i imax += 1 if (event.area.y + event.area.height).to_i % @char_height != 0 imax = [ imax, @vis_lines ].min render_hex_lines(imin, imax) end @xdisp.signal_connect 'scroll_event' do |xdisp, event| @scrollbar.event(event) end @xdisp.signal_connect 'button_press_event' do |xdisp, event| mouse_handler[xdisp, event] end @xdisp.signal_connect 'button_release_event' do |xdisp, event| mouse_handler[xdisp, event] end @xdisp.signal_connect 'motion_notify_event' do |xdisp, event| _w, x, y, _m = xdisp.window.pointer if y < 0 @scroll_dir = -1 elsif y >= xdisp.allocation.height @scroll_dir = 1 else @scroll_dir = 0 end if @scroll_dir != 0 if @scroll_timeout == nil @scroll_timeout = GLib::Timeout.add(SCROLL_TIMEOUT) { if @scroll_dir < 0 set_cursor([ 0, @cursor_pos - @cpl ].max) elsif @scroll_dir > 0 set_cursor([ @data.size - 1, @cursor_pos + @cpl ].min) end true } next end else if @scroll_timeout != nil GLib::Source.remove @scroll_timeout @scroll_timeout = nil end end next if event.window != xdisp.window hex_to_pointer(x,y) if @active_view == View::HEX and @button == 1 end put @xdisp, 0, 0 @xdisp.show @adisp = DrawingArea.new @adisp.modify_font @font_desc @alayout = @adisp.create_pango_layout('') @adisp.events = Gdk::Event::EXPOSURE_MASK | Gdk::Event::BUTTON_PRESS_MASK | Gdk::Event::BUTTON_RELEASE_MASK | Gdk::Event::BUTTON_MOTION_MASK | Gdk::Event::SCROLL_MASK @adisp.signal_connect 'realize' do @adisp_gc = Gdk::GC.new(@adisp.window) @adisp_gc.set_exposures(true) end @adisp.signal_connect 'expose_event' do |adisp, event| imin = (event.area.y / @char_height).to_i imax = ((event.area.y + event.area.height) / @char_height).to_i imax += 1 if (event.area.y + event.area.height).to_i % @char_height != 0 imax = [ imax, @vis_lines ].min render_ascii_lines(imin, imax) end @adisp.signal_connect 'scroll_event' do |adisp, event| @scrollbar.event(event) end @adisp.signal_connect 'button_press_event' do |adisp, event| mouse_handler[adisp, event] end @adisp.signal_connect 'button_release_event' do |adisp, event| mouse_handler[adisp, event] end @adisp.signal_connect 'motion_notify_event' do |adisp, event| _w, x, y, _m = adisp.window.pointer if y < 0 @scroll_dir = -1 elsif y >= adisp.allocation.height @scroll_dir = 1 else @scroll_dir = 0 end if @scroll_dir != 0 if @scroll_timeout == nil @scroll_timeout = GLib::Timeout.add(SCROLL_TIMEOUT) { if @scroll_dir < 0 set_cursor([ 0, @cursor_pos - @cpl ].max) elsif @scroll_dir > 0 set_cursor([ @data.size - 1, @cursor_pos + @cpl ].min) end true } next end else if @scroll_timeout != nil GLib::Source.remove @scroll_timeout @scroll_timeout = nil end end next if event.window != adisp.window ascii_to_pointer(x,y) if @active_view == View::ASCII and @button == 1 end put @adisp, 0, 0 @adisp.show @adj = Gtk::Adjustment.new(0, 0, 0, 0, 0, 0) @scrollbar = Gtk::VScrollbar.new(@adj) @adj.signal_connect 'value_changed' do |adj| unless @xdisp_gc.nil? or @adisp_gc.nil? or not @xdisp.drawable? or not @adisp.drawable? source_min = (adj.value.to_i - @top_line) * @char_height source_max = source_min + @xdisp.allocation.height dest_min = 0 dest_max = @xdisp.allocation.height rect = Gdk::Rectangle.new(0, 0, 0, 0) @top_line = adj.value.to_i if source_min < 0 rect.y = 0 rect.height = -source_min rect_height = [ rect.height, @xdisp.allocation.height ].min source_min = 0 dest_min = rect.height else rect.y = 2 * @xdisp.allocation.height - source_max rect.y = 0 if rect.y < 0 rect.height = @xdisp.allocation.height - rect.y source_max = @xdisp.allocation.height dest_max = rect.y end if source_min != source_max @xdisp.window.draw_drawable( @xdisp_gc, @xdisp.window, 0, source_min, 0, dest_min, @xdisp.allocation.width, source_max - source_min ) @adisp.window.draw_drawable( @adisp_gc, @adisp.window, 0, source_min, 0, dest_min, @adisp.allocation.width, source_max - source_min ) if @offsets if @offsets_gc.nil? @offsets_gc = Gdk::GC.new(@offsets.window) @offsets_gc.set_exposures(true) end @offsets.window.draw_drawable( @offsets_gc, @offsets.window, 0, source_min, 0, dest_min, @offsets.allocation.width, source_max - source_min ) end # TODO update_all_auto_highlights(true, true) invalidate_all_highlights rect.width = @xdisp.allocation.width @xdisp.window.invalidate(rect, false) rect.width = @adisp.allocation.width @adisp.window.invalidate(rect, false) if @offsets rect.width = @offsets.allocation.width @offsets.window.invalidate(rect, false) end end end end put @scrollbar, 0, 0 @scrollbar.show end def set_selection(s, e) e = [ e, @data.size ].min @@primary.clear if @selection.start != @selection.end os, oe = [ @selection.start, @selection.end ].sort @selection.start = [ 0, s ].max @selection.start = [ @selection.start, @data.size ].min @selection.end = [ e, @data.size ].min invalidate_highlight(@selection) ns, ne = [ @selection.start, @selection.end ].sort if ns != os and ne != oe bytes_changed([ns, os].min, [ne, oe].max) elsif ne != oe bytes_changed(*[ne, oe].sort) elsif ns != os bytes_changed(*[ns, os].sort) end if @selection.start != @selection.end if @active_view == View::HEX brk_len = 2 * @cpl + @cpl / @group_type format_xblock(s,e) (@disp_buffer.size / brk_len + 1).times do |i| @disp_buffer.insert(i * (brk_len + 1), $/) end else brk_len = @cpl format_ablock(s,e) end @@primary.set_text(@disp_buffer) end end def get_selection [ @selection.start, @selection.end ].sort end def clear_selection set_selection(0, 0) end def cursor @cursor_pos end def set_cursor(index) return if index < 0 or index > @data.size old_pos = @cursor_pos index -= 1 if @insert and index == @data.size index = [ 0, index ].max hide_cursor @cursor_pos = index return if @cpl == 0 y = index / @cpl if y >= @top_line + @vis_lines @adj.value = [ y - @vis_lines + 1, @lines - @vis_lines ].min @adj.value = [ 0, @adj.value ].max @adj.signal_emit 'value_changed' elsif y < @top_line @adj.value = y @adj.signal_emit 'value_changed' end @lower_nibble = false if index == @data.size if @selecting set_selection(@selection.start, @cursor_pos) bytes_changed(*[@cursor_pos, old_pos].sort) else# @selection.start != @selection.end s, e = [@selection.start, @selection.end].sort @selection.end = @selection.start = @cursor_pos bytes_changed(s, e) end self.signal_emit 'cursor_moved' bytes_changed(old_pos, old_pos) show_cursor end def set_cursor_xy(x, y) pos = y.to_i * @cpl + x.to_i return if y < 0 or y >= @lines or x < 0 or x >= @cpl or pos > @data.size set_cursor(pos) end def set_cursor_on_lower_nibble(bool) if @selecting bytes_changed(@cursor_pos, @cursor_pos) @lower_nibble = bool elsif @selection.start != @selection.end s, e = [ @selection.start, @selection.end ].sort @selection.start = @selection.end = 0 bytes_changed(s, e) @lower_nibble = bool else hide_cursor @lower_nibble = bool show_cursor end end def set_group_type(type) hide_cursor @group_type = type recalc_displays(self.allocation.width, self.allocation.height) self.queue_resize show_cursor end def show_offsets(bool) return unless @show_offsets ^ bool @show_offsets = bool if bool show_offsets_widget else hide_offsets_widget end end def set_font(fontname) @font_desc = Pango::FontDescription.new(fontname) @disp_font_metrics = load_font(fontname) @xdisp.modify_font(@font_desc) if @xdisp @adisp.modify_font(@font_desc) if @adisp @offsets.modify_font(@font_desc) if @offsets @char_width = get_max_char_width(@disp_font_metrics) @char_height = Pango.pixels(@disp_font_metrics.ascent) + Pango.pixels(@disp_font_metrics.descent) + 2 recalc_displays(self.allocation.width, self.allocation.height) redraw_widget end def set_data(data) prev_data_size = @data.size @data = data.dup recalc_displays(self.allocation.width, self.allocation.height) set_cursor 0 bytes_changed(0, [ prev_data_size, @data.size ].max) redraw_widget end def validate_highlight(hl) unless hl.valid hl.start_line = [ hl.start, hl.end ].min / @cpl - @top_line hl.end_line = [ hl.start, hl.end ].max / @cpl - @top_line hl.valid = true end end def invalidate_highlight(hl) hl.valid = false end def invalidate_all_highlights @highlights.each do |hl| invalidate_highlight(hl) end end private signal_new( 'data_changed', GLib::Signal::RUN_FIRST, nil, nil, String ) signal_new( 'cursor_moved', GLib::Signal::RUN_FIRST, nil, nil ) def signal_do_cursor_moved end def signal_do_data_changed(data) # TODO end def signal_do_realize super self.window.set_back_pixmap(nil, true) end def signal_do_size_allocate(alloc) hide_cursor recalc_displays(alloc.width, alloc.height) self.set_allocation(alloc.x, alloc.y, alloc.width, alloc.height) self.window.move_resize( alloc.x, alloc.y, alloc.width, alloc.height ) if self.realized? bw = self.border_width xt = widget_get_xt yt = widget_get_yt my_alloc = Gtk::Allocation.new(0, 0, 0, 0) my_alloc.x = bw + xt my_alloc.y = bw + yt my_alloc.height = [ alloc.height - 2*bw - 2*yt, 1 ].max if @show_offsets my_alloc.width = 8 * @char_width @offsets.size_allocate(my_alloc) @offsets.queue_draw my_alloc.x += 2*xt + my_alloc.width end my_alloc.width = @xdisp_width @xdisp.size_allocate(my_alloc) my_alloc.x = alloc.width - bw - @scrollbar.requisition[0] my_alloc.y = bw my_alloc.width = @scrollbar.requisition[0] my_alloc.height = [ alloc.height - 2*bw, 1 ].max @scrollbar.size_allocate(my_alloc) my_alloc.x -= @adisp_width + xt my_alloc.y = bw + yt my_alloc.width = @adisp_width my_alloc.height = [ alloc.height - 2*bw - 2*yt, 1 ].max @adisp.size_allocate(my_alloc) show_cursor end def signal_do_size_request(req) sb_width, _sb_height = @scrollbar.size_request bw = self.border_width xt, yt = widget_get_xt, widget_get_yt width = 4*xt + 2*bw + sb_width + @char_width*(DEFAULT_CPL + (DEFAULT_CPL-1)/@group_type) width += 2*xt + 8*@char_width if @show_offsets height = DEFAULT_LINES * @char_height + 2*yt + 2*bw req[0] = width req[1] = height end def signal_do_expose_event(event) draw_shadow(event.area) super(event) true end def signal_do_key_press_event(event) hide_cursor @selecting = (event.state & Gdk::Window::SHIFT_MASK) != 0 ret = true case event.keyval when Gdk::Keyval::GDK_KP_Tab, Gdk::Keyval::GDK_Tab @active_view = (@active_view == View::HEX) ? View::ASCII : View::HEX when Gdk::Keyval::GDK_Up set_cursor(@cursor_pos - @cpl) when Gdk::Keyval::GDK_Down set_cursor(@cursor_pos + @cpl) when Gdk::Keyval::GDK_Page_Up set_cursor([0, @cursor_pos - @vis_lines * @cpl].max) when Gdk::Keyval::GDK_Page_Down set_cursor([@cursor_pos + @vis_lines * @cpl, @data.size].min) when Gdk::Keyval::GDK_Left if @active_view == View::HEX if @selecting set_cursor(@cursor_pos - 1) else @lower_nibble ^= 1 set_cursor(@cursor_pos - 1) if @lower_nibble end else set_cursor(@cursor_pos - 1) end when Gdk::Keyval::GDK_Right if @active_view == View::HEX if @selecting set_cursor(@cursor_pos + 1) else @lower_nibble ^= 1 set_cursor(@cursor_pos + 1) unless @lower_nibble end else set_cursor(@cursor_pos + 1) end when Gdk::Keyval::GDK_c, Gdk::Keyval::GDK_C if event.state & Gdk::Window::CONTROL_MASK != 0 s,e = @selection.start, @selection.end + 1 if @active_view == View::HEX brk_len = 2 * @cpl + @cpl / @group_type format_xblock(s,e) (@disp_buffer.size / brk_len + 1).times do |i| @disp_buffer.insert(i * (brk_len + 1), $/) end else brk_len = @cpl format_ablock(s,e) end @@clipboard.set_text(@disp_buffer) end else ret = false end show_cursor ret end def hex_to_pointer(mx, my) cy = @top_line + my.to_i / @char_height cx = x = 0 while cx < 2 * @cpl x += @char_width if x > mx set_cursor_xy(cx / 2, cy) set_cursor_on_lower_nibble(cx % 2 != 0) cx = 2 * @cpl end cx += 1 x += @char_width if ( cx % (2 * @group_type) == 0 ) end end def ascii_to_pointer(mx, my) cx = mx / @char_width cy = @top_line + my / @char_height set_cursor_xy(cx, cy) end def load_font(fontname) desc = Pango::FontDescription.new(fontname) context = Gdk::Pango.context context.set_language(Gtk.default_language) font = context.load_font(desc) font.metrics(context.language) end def draw_shadow(area) bw = self.border_width x = bw xt = widget_get_xt if @show_offsets self.style.paint_shadow( self.window, Gtk::STATE_NORMAL, Gtk::SHADOW_IN, nil, self, nil, bw, bw, 8*@char_width + 2*xt, self.allocation.height - 2*bw ) x += 8*@char_width + 2*xt end self.style.paint_shadow( self.window, Gtk::STATE_NORMAL, Gtk::SHADOW_IN, nil, self, nil, x, bw, @xdisp_width + 2*xt, self.allocation.height - 2*bw ) self.style.paint_shadow( self.window, Gtk::STATE_NORMAL, Gtk::SHADOW_IN, nil, self, nil, self.allocation.width - bw - @adisp_width - @scrollbar.requisition[0] - 2*xt, bw, @adisp_width + 2*xt, self.allocation.height - 2*bw ) end def redraw_widget return unless self.realized? self.window.invalidate(nil, false) end def widget_get_xt self.style.xthickness end def widget_get_yt self.style.ythickness end def recalc_displays(width, height) old_cpl = @cpl w, _h = @scrollbar.size_request @xdisp_width = 1 @adisp_width = 1 total_width = width - 2 * self.border_width - 4 * widget_get_xt - w total_width -= 2 * widget_get_xt + 8 * @char_width if @show_offsets total_cpl = total_width / @char_width if total_cpl == 0 or total_width < 0 @cpl = @lines = @vis_lines = 0 return end @cpl = 0 begin break if @cpl % @group_type == 0 and total_cpl < @group_type * 3 @cpl += 1 total_cpl -= 3 total_cpl -= 1 if @cpl % @group_type == 0 end while total_cpl > 0 return if @cpl == 0 if @data.empty? @lines = 1 else @lines = @data.size / @cpl @lines += 1 if @data.size % @cpl != 0 end @vis_lines = (height - 2*self.border_width - 2*widget_get_yt).to_i / @char_height.to_i @adisp_width = @cpl * @char_width + 1 xcpl = @cpl * 2 + (@cpl - 1) / @group_type @xdisp_width = xcpl * @char_width + 1 @disp_buffer = '' @adj.value = [@top_line * old_cpl / @cpl, @lines - @vis_lines].min @adj.value = [ 0, @adj.value ].max if @cursor_pos / @cpl < @adj.value or @cursor_pos / @cpl > @adj.value + @vis_lines - 1 @adj.value = [ @cursor_pos / @cpl, @lines - @vis_lines ].min @adj.value = [ 0, @adj.value ].max end @adj.lower = 0 @adj.upper = @lines @adj.step_increment = 1 @adj.page_increment = @vis_lines - 1 @adj.page_size = @vis_lines @adj.signal_emit 'changed' @adj.signal_emit 'value_changed' end def get_max_char_width(metrics) layout = self.create_pango_layout('') layout.set_font_description(@font_desc) char_widths = [ 0 ] (1..100).each do |i| logical_rect = Pango::Rectangle.new(0, 0, 0, 0) if is_displayable(i.chr) layout.set_text(i.chr) logical_rect = layout.pixel_extents[1] end char_widths << logical_rect.width end char_widths[48..122].max end def show_cursor unless @cursor_shown if @xdisp_gc and @adisp_gc and @xdisp.realized? and @adisp.realized? render_xc render_ac end @cursor_shown = true end end def hide_cursor if @cursor_shown if @xdisp_gc and @adisp_gc and @xdisp.realized? and @adisp.realized? render_byte(@cursor_pos) end @cursor_shown = false end end def show_offsets_widget @offsets = DrawingArea.new @offsets.modify_font @font_desc @olayout = @offsets.create_pango_layout('') @offsets.events = Gdk::Event::EXPOSURE_MASK @offsets.signal_connect 'expose_event' do |offsets, event| imin = (event.area.y / @char_height).to_i imax = ((event.area.y + event.area.height) / @char_height).to_i imax += 1 if (event.area.y + event.area.height).to_i % @char_height != 0 imax = [ imax, @vis_lines ].min render_offsets(imin, imax) end put @offsets, 0, 0 @offsets.show end def hide_offsets_widget if @offsets self.remove(@offsets) @offsets = @offsets_gc = nil end end def is_displayable(c) c = c.ord c >= 0x20 and c < 0x7f end def bytes_changed(s, e) start_line = s / @cpl - @top_line end_line = e / @cpl - @top_line return if end_line < 0 or start_line > @vis_lines start_line = [ 0, start_line ].max render_hex_lines(start_line, end_line) render_ascii_lines(start_line, end_line) render_offsets(start_line, end_line) if @show_offsets end def render_hex_highlights(cursor_line) xcpl = @cpl * 2 + @cpl / @group_type @highlights.each do |hl| next if (hl.start - hl.end).abs < hl.min_select validate_highlight(hl) s, e = [ hl.start, hl.end ].sort sl, el = hl.start_line, hl.end_line hl.style.attach(@xdisp.window) if hl.style state = (@active_view == View::HEX) ? Gtk::STATE_SELECTED : Gtk::STATE_INSENSITIVE if cursor_line == sl cursor_off = 2 * (s % @cpl) + (s % @cpl) / @group_type if cursor_line == el len = 2 * (e % @cpl + 1) + (e % @cpl) / @group_type else len = xcpl end len -= cursor_off (hl.style || self.style).paint_flat_box( @xdisp.window, state, Gtk::SHADOW_NONE, nil, @xdisp, '', cursor_off * @char_width, cursor_line * @char_height, len * @char_width, @char_height ) if len > 0 elsif cursor_line == el cursor_off = 2 * (e % @cpl + 1) + (e % @cpl) / @group_type (hl.style || self.style).paint_flat_box( @xdisp.window, state, Gtk::SHADOW_NONE, nil, @xdisp, '', 0, cursor_line * @char_height, cursor_off * @char_width, @char_height ) if cursor_off > 0 elsif cursor_line > sl and cursor_line < el (hl.style || self.style).paint_flat_box( @xdisp.window, state, Gtk::SHADOW_NONE, nil, @xdisp, '', 0, cursor_line * @char_height, xcpl * @char_width, @char_height ) end hl.style.attach(@adisp.window) if hl.style end end def render_hex_lines(imin, imax) return unless self.realized? and @cpl != 0 cursor_line = @cursor_pos / @cpl - @top_line @xdisp_gc.set_foreground(self.style.base(Gtk::STATE_NORMAL)) @xdisp.window.draw_rectangle( @xdisp_gc, true, 0, imin * @char_height, @xdisp.allocation.width, (imax - imin + 1) * @char_height ) imax = [ imax, @vis_lines, @lines ].min @xdisp_gc.set_foreground(self.style.text(Gtk::STATE_NORMAL)) frm_len = format_xblock((@top_line+imin) * @cpl, [(@top_line+imax+1) * @cpl, @data.size].min) tmp = nil xcpl = @cpl*2 + @cpl/@group_type (imin..imax).each do |i| return unless (tmp = frm_len - ((i - imin) * xcpl)) > 0 render_hex_highlights(i) text = @disp_buffer[(i-imin) * xcpl, [xcpl, tmp].min] @xlayout.set_text(text) @xdisp.window.draw_layout(@xdisp_gc, 0, i * @char_height, @xlayout) end render_xc if cursor_line >= imin and cursor_line <= imax and @cursor_shown end def render_ascii_highlights(cursor_line) @highlights.each do |hl| next if (hl.start - hl.end).abs < hl.min_select validate_highlight(hl) s, e = [ hl.start, hl.end ].sort sl, el = hl.start_line, hl.end_line hl.style.attach(@adisp.window) if hl.style state = (@active_view == View::ASCII) ? Gtk::STATE_SELECTED : Gtk::STATE_INSENSITIVE if cursor_line == sl cursor_off = s % @cpl len = if cursor_line == el e - s + 1 else @cpl - cursor_off end (hl.style || self.style).paint_flat_box( @adisp.window, state, Gtk::SHADOW_NONE, nil, @adisp, '', cursor_off * @char_width, cursor_line * @char_height, len * @char_width, @char_height ) if len > 0 elsif cursor_line == el cursor_off = e % @cpl + 1 (hl.style || self.style).paint_flat_box( @adisp.window, state, Gtk::SHADOW_NONE, nil, @adisp, '', 0, cursor_line * @char_height, cursor_off * @char_width, @char_height ) if cursor_off > 0 elsif cursor_line > sl and cursor_line < el (hl.style || self.style).paint_flat_box( @adisp.window, state, Gtk::SHADOW_NONE, nil, @adisp, '', 0, cursor_line * @char_height, @cpl * @char_width, @char_height ) end hl.style.attach(@adisp.window) if hl.style end end def render_ascii_lines(imin, imax) return unless self.realized? and @cpl != 0 cursor_line = @cursor_pos / @cpl - @top_line @adisp_gc.set_foreground(self.style.base(Gtk::STATE_NORMAL)) @adisp.window.draw_rectangle( @adisp_gc, true, 0, imin * @char_height, @adisp.allocation.width, (imax - imin + 1) * @char_height ) imax = [ imax, @vis_lines, @lines ].min @adisp_gc.set_foreground(self.style.text(Gtk::STATE_NORMAL)) frm_len = format_ablock((@top_line+imin) * @cpl, [(@top_line+imax+1) * @cpl, @data.size].min) tmp = nil (imin..imax).each do |i| return unless (tmp = frm_len - ((i - imin) * @cpl)) > 0 render_ascii_highlights(i) text = @disp_buffer[(i-imin) * @cpl, [@cpl, tmp].min] @alayout.set_text(text) @adisp.window.draw_layout(@adisp_gc, 0, i * @char_height, @alayout) end render_ac if cursor_line >= imin and cursor_line <= imax and @cursor_shown end def render_offsets(imin, imax) return unless self.realized? unless @offsets_gc @offsets_gc = Gdk::GC.new(@offsets.window) @offsets_gc.set_exposures(true) end @offsets_gc.set_foreground(self.style.base(Gtk::STATE_INSENSITIVE)) @offsets.window.draw_rectangle( @offsets_gc, true, 0, imin * @char_height, @offsets.allocation.width, (imax - imin + 1) * @char_height ) imax = [ imax, @vis_lines, @lines - @top_line - 1 ].min @offsets_gc.set_foreground(self.style.text(Gtk::STATE_NORMAL)) (imin..imax).each do |i| text = "%08x" % ((@top_line + i) * @cpl + @starting_offset) @olayout.set_text(text) @offsets.window.draw_layout(@offsets_gc, 0, i * @char_height, @olayout) end end def render_byte(pos) return unless @xdisp_gc and @adisp_gc and @xdisp.realized? and @adisp.realized? return unless (coords = get_xcoords(pos)) cx, cy = coords c = format_xbyte(pos) @xdisp_gc.set_foreground(self.style.base(Gtk::STATE_NORMAL)) @xdisp.window.draw_rectangle( @xdisp_gc, true, cx, cy, 2 * @char_width, @char_height ) if pos < @data.size @xdisp_gc.set_foreground(self.style.text(Gtk::STATE_NORMAL)) @xlayout.set_text(c) @xdisp.window.draw_layout(@xdisp_gc, cx, cy, @xlayout) end return unless (coords = get_acoords(pos)) cx, cy = coords @adisp_gc.set_foreground(self.style.base(Gtk::STATE_NORMAL)) @adisp.window.draw_rectangle( @adisp_gc, true, cx, cy, @char_width, @char_height ) if pos < @data.size @adisp_gc.set_foreground(self.style.text(Gtk::STATE_NORMAL)) c = get_byte(pos) c = '.' unless is_displayable(c) @alayout.set_text(c) @adisp.window.draw_layout(@adisp_gc, cx, cy, @alayout) end end def render_xc return unless @xdisp.realized? if coords = get_xcoords(@cursor_pos) cx, cy = coords c = format_xbyte(@cursor_pos) if @lower_nibble cx += @char_width c = c[1,1] else c = c[0,1] end @xdisp_gc.set_foreground(self.style.base(Gtk::STATE_ACTIVE)) @xdisp.window.draw_rectangle( @xdisp_gc, (@active_view == View::HEX), cx, cy, @char_width, @char_height - 1 ) @xdisp_gc.set_foreground(self.style.text(Gtk::STATE_ACTIVE)) @xlayout.set_text(c) @xdisp.window.draw_layout(@xdisp_gc, cx, cy, @xlayout) end end def render_ac return unless @adisp.realized? if coords = get_acoords(@cursor_pos) cx, cy = coords c = get_byte(@cursor_pos) c = '.' unless is_displayable(c) @adisp_gc.set_foreground(self.style.base(Gtk::STATE_ACTIVE)) @adisp.window.draw_rectangle( @adisp_gc, (@active_view == View::ASCII), cx, cy, @char_width, @char_height - 1 ) @adisp_gc.set_foreground(self.style.text(Gtk::STATE_ACTIVE)) @alayout.set_text(c) @adisp.window.draw_layout(@adisp_gc, cx, cy, @alayout) end end def get_xcoords(pos) return nil if @cpl == 0 cy = pos / @cpl - @top_line return nil if cy < 0 cx = 2 * (pos % @cpl) spaces = (pos % @cpl) / @group_type cx *= @char_width cy *= @char_height spaces *= @char_width [cx + spaces, cy] end def get_acoords(pos) return nil if @cpl == 0 cy = pos / @cpl - @top_line return nil if cy < 0 cy *= @char_height cx = @char_width * (pos % @cpl) [cx, cy] end def format_xblock(s, e) @disp_buffer = '' (s+1..e).each do |i| @disp_buffer << get_byte(i - 1).unpack('H2')[0] @disp_buffer << ' ' if i % @group_type == 0 end @disp_buffer.size end def format_ablock(s, e) @disp_buffer = '' (s..e-1).each do |i| c = get_byte(i) c = '.' unless is_displayable(c) @disp_buffer << c end @disp_buffer.size end def get_byte(offset) if offset >= 0 and offset < @data.size @data[offset, 1] else 0.chr end end def format_xbyte(pos) get_byte(pos).unpack('H2')[0] end end end __END__ hexedit = Gtk::HexEditor.new(File.read '/bin/cat') hexedit.show_offsets(true) hexedit.set_cursor 2 hexedit.set_cursor_on_lower_nibble true hexedit.set_font 'Terminus 12' hexedit.set_group_type Gtk::HexEditor::Group::LONG window = Gtk::Window.new window.add(hexedit) window.show_all Gtk.main origami-2.0.0/bin/gui/imgview.rb0000644000004100000410000000371612757133666016547 0ustar www-datawww-data=begin This file is part of PDF Walker, a graphical PDF file browser Copyright (C) 2016 Guillaume Delugré. PDF Walker 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. PDF Walker 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 PDF Walker. If not, see . =end module PDFWalker class ImgViewer < Window attr_reader :image def initialize super() set_title "Image view" set_decorated false set_resizable false add_events(Gdk::Event::KEY_RELEASE_MASK) signal_connect('key_release_event') { |w, event| destroy if event.keyval == Gdk::Keyval::GDK_Escape } end def show_raw_img(data, w, h, bpc, bpr) set_default_size w,h pixbuf = GdkPixbuf::Pixbuf.new data: data, colorspace: GdkPixbuf::Colorspace::RGB, has_alpha: false, bits_per_sample: bpc, width: w, height: h, row_stride: bpr @image = Gtk::Image.new(pixbuf) add @image show_all end def show_compressed_img(data) loader = Gdk::PixbufLoader.new loader.last_write data pixbuf = loader.pixbuf set_default_size pixbuf.width, pixbuf.height @image = Gtk::Image.new(pixbuf) add @image show_all end end end origami-2.0.0/bin/gui/about.rb0000644000004100000410000000231312757133666016202 0ustar www-datawww-data=begin This file is part of PDF Walker, a graphical PDF file browser Copyright (C) 2016 Guillaume Delugré. PDF Walker 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. PDF Walker 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 PDF Walker. If not, see . =end module PDFWalker class Walker < Window def about AboutDialog.show(self, name: "PDF Walker", program_name: "PDF Walker", version: Origami::VERSION, copyright: "Copyright (C) 2016\nGuillaume Delugré", comments: "A graphical PDF parser front-end", license: File.read(File.join(__dir__, "COPYING")) ) end end end origami-2.0.0/bin/gui/textview.rb0000644000004100000410000000565612757133666016764 0ustar www-datawww-data=begin This file is part of PDF Walker, a graphical PDF file browser Copyright (C) 2016 Guillaume Delugré. PDF Walker 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. PDF Walker 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 PDF Walker. If not, see . =end module PDFWalker class Walker < Window private def create_objectview @objectview = ObjectView.new(self) end class ObjectView < Notebook attr_reader :parent attr_reader :pdfpanel, :valuepanel def initialize(parent) @parent = parent super() @pdfbuffer = TextBuffer.new @pdfview = TextView.new(@pdfbuffer).set_editable(false).set_cursor_visible(false).set_left_margin(5) @pdfpanel = ScrolledWindow.new.set_policy(POLICY_AUTOMATIC, POLICY_AUTOMATIC) @pdfpanel.add_with_viewport @pdfview append_page(@pdfpanel, Label.new("PDF Code")) @pdfbuffer.create_tag("Default", weight: Pango::WEIGHT_BOLD, family: "monospace", scale: Pango::AttrScale::LARGE ) end def load(object) begin self.clear case object when Origami::PDF::Header, Origami::FDF::Header, Origami::PPKLite::Header text = object.to_s @pdfbuffer.set_text(text) @pdfbuffer.apply_tag("Default", @pdfbuffer.start_iter, @pdfbuffer.end_iter) when Origami::Object if object.is_a?(Origami::Stream) text = [ "#{object.no} #{object.generation} obj", object.dictionary ].join($/) else text = object.to_s end text.encode!("UTF-8", replace: '.') .gsub!("\x00", '.') @pdfbuffer.set_text(text) @pdfbuffer.apply_tag("Default", @pdfbuffer.start_iter, @pdfbuffer.end_iter) end rescue @parent.error("An error occured while loading this object.\n#{$!} (#{$!.class})") end end def clear @pdfbuffer.set_text("") end end end end origami-2.0.0/bin/gui/properties.rb0000644000004100000410000001141212757133666017264 0ustar www-datawww-data=begin This file is part of PDF Walker, a graphical PDF file browser Copyright (C) 2016 Guillaume Delugré. PDF Walker 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. PDF Walker 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 PDF Walker. If not, see . =end require 'digest/md5' module PDFWalker class Walker < Window def display_file_properties Properties.new(self, @opened) if @opened end class Properties < Dialog @@acrobat_versions = { 1.0 => "1.x", 1.1 => "2.x", 1.2 => "3.x", 1.3 => "4.x", 1.4 => "5.x", 1.5 => "6.x", 1.6 => "7.x", 1.7 => "8.x / 9.x / 10.x" } def initialize(parent, pdf) super("Document properties", parent, Dialog::MODAL, [Stock::CLOSE, Dialog::RESPONSE_NONE]) docframe = Frame.new(" File properties ") stat = File.stat(parent.filename) creation_date = stat.ctime.to_s.encode("utf-8", :invalid => :replace, :undef => :replace) last_modified = stat.mtime.to_s.encode("utf-8", :invalid => :replace, :undef => :replace) md5sum = Digest::MD5.hexdigest(File.binread(parent.filename)) labels = [ [ "Filename:", parent.filename ], [ "File size:", "#{File.size(parent.filename)} bytes" ], [ "MD5:", md5sum ], [ "Read-only:", "#{not stat.writable?}" ], [ "Creation date:", creation_date ], [ "Last modified:", last_modified ] ] doctable = Table.new(labels.size + 1, 3) row = 0 labels.each do |name, value| doctable.attach(Label.new(name).set_alignment(1,0), 0, 1, row, row + 1, Gtk::EXPAND | Gtk::FILL, Gtk::SHRINK, 4, 4) doctable.attach(Label.new(value).set_alignment(0,0), 1, 2, row, row + 1, Gtk::EXPAND | Gtk::FILL, Gtk::SHRINK, 4, 4) row = row.succ end docframe.border_width = 5 docframe.shadow_type = Gtk::SHADOW_IN docframe.add(doctable) pdfframe = Frame.new(" PDF properties ") pdf_version = pdf.header.to_f if pdf_version >= 1.0 and pdf_version <= 1.7 acrobat_version = @@acrobat_versions[pdf_version] else acrobat_version = "unknown version" end labels = [ [ "Version:", "#{pdf_version} (Acrobat #{acrobat_version})" ], [ "Number of revisions:", "#{pdf.revisions.size}" ], [ "Number of indirect objects:", "#{pdf.indirect_objects.size}" ], [ "Number of pages:", "#{pdf.pages.count}" ], [ "Linearized:", pdf.linearized? ? 'yes' : 'no' ], [ "Encrypted:", pdf.encrypted? ? 'yes' : 'no' ], [ "Signed:", pdf.signed? ? 'yes' : 'no' ], [ "Has usage rights:", pdf.usage_rights? ? 'yes' : 'no' ], [ "Form:", pdf.form? ? 'yes' : 'no' ], [ "XFA form:", pdf.xfa_form? ? 'yes' : 'no' ], [ "Document information:", pdf.document_info? ? 'yes' : 'no' ], [ "Metadata:", pdf.metadata? ? 'yes' : 'no' ] ] pdftable = Table.new(labels.size + 1, 3) row = 0 labels.each do |name, value| pdftable.attach(Label.new(name).set_alignment(1,0), 0, 1, row, row + 1, Gtk::FILL, Gtk::SHRINK, 4, 4) pdftable.attach(Label.new(value).set_alignment(0,0), 1, 2, row, row + 1, Gtk::EXPAND | Gtk::FILL, Gtk::SHRINK, 4, 4) row = row.succ end pdfframe.border_width = 5 pdfframe.shadow_type = Gtk::SHADOW_IN pdfframe.add(pdftable) vbox.add(docframe) vbox.add(pdfframe) signal_connect('response') { destroy } show_all end end end end origami-2.0.0/bin/gui/file.rb0000644000004100000410000003625212757133666016020 0ustar www-datawww-data=begin This file is part of PDF Walker, a graphical PDF file browser Copyright (C) 2016 Guillaume Delugré. PDF Walker 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. PDF Walker 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 PDF Walker. If not, see . =end require 'origami' module PDFWalker class Walker < Window attr_reader :opened attr_reader :explore_history def close @opened = nil @filename = '' @explorer_history.clear @treeview.clear @objectview.clear @hexview.clear # disable all menus. [ @file_menu_close, @file_menu_saveas, @file_menu_serialize, @file_menu_refresh, @document_menu_search, @document_menu_gotocatalog, @document_menu_gotodocinfo, @document_menu_gotometadata, @document_menu_gotopage, @document_menu_gotofield, @document_menu_gotorev, @document_menu_gotoobj, @document_menu_properties, @document_menu_sign, @document_menu_ur ].each do |menu| menu.sensitive = false end @statusbar.pop(@main_context) GC.start end def open(filename = nil) dialog = Gtk::FileChooserDialog.new("Open PDF File", self, FileChooser::ACTION_OPEN, nil, [Stock::CANCEL, Dialog::RESPONSE_CANCEL], [Stock::OPEN, Dialog::RESPONSE_ACCEPT]) last_file = @config.recent_files.first unless last_file.nil? last_folder = File.dirname(last_file) dialog.set_current_folder(last_folder) if File.directory?(last_folder) end dialog.filter = FileFilter.new.add_pattern("*.acrodata").add_pattern("*.pdf").add_pattern("*.fdf") if filename.nil? and dialog.run != Gtk::Dialog::RESPONSE_ACCEPT dialog.destroy return end create_progressbar filename ||= dialog.filename dialog.destroy begin if @help_menu_profile.active? require 'ruby-prof' RubyProf.start end target = parse_file(filename) if @help_menu_profile.active? result = RubyProf.stop multiprinter = RubyProf::MultiPrinter.new(result) Dir.mkdir(@config.profile_output_dir) unless Dir.exists?(@config.profile_output_dir) multiprinter.print(path: @config.profile_output_dir, profile: File.basename(filename)) end if target close if @opened @opened = target @filename = filename @config.last_opened_file(filename) @config.save update_recent_menu @last_search_result = [] @last_search = { :expr => "", :regexp => false, :type => :body } self.reload # Enable basic file menus. [ @file_menu_close, @file_menu_refresh, ].each do |menu| menu.sensitive = true end @explorer_history.clear @statusbar.push(@main_context, "Viewing #{filename}") if @opened.is_a?(Origami::PDF) # Enable save and document menu. [ @file_menu_saveas, @file_menu_serialize, @document_menu_search, @document_menu_gotocatalog, @document_menu_gotopage, @document_menu_gotorev, @document_menu_gotoobj, @document_menu_properties, @document_menu_sign, @document_menu_ur ].each do |menu| menu.sensitive = true end @document_menu_gotodocinfo.sensitive = true if @opened.document_info? @document_menu_gotometadata.sensitive = true if @opened.metadata? @document_menu_gotofield.sensitive = true if @opened.form? page_menu = Menu.new @document_menu_gotopage.remove_submenu @opened.each_page.with_index(1) do |page, index| page_menu.append(item = MenuItem.new(index.to_s).show) item.signal_connect("activate") do @treeview.goto(page) end end @document_menu_gotopage.set_submenu(page_menu) field_menu = Menu.new @document_menu_gotofield.remove_submenu @opened.each_field do |field| field_name = field.T || "" field_menu.append(item = MenuItem.new(field_name).show) item.signal_connect("activate") do @treeview.goto(field) end end @document_menu_gotofield.set_submenu(field_menu) rev_menu = Menu.new @document_menu_gotorev.remove_submenu rev_index = 1 @opened.revisions.each do |rev| rev_menu.append(item = MenuItem.new(rev_index.to_s).show) item.signal_connect("activate") do @treeview.goto(rev) end rev_index = rev_index + 1 end @document_menu_gotorev.set_submenu(rev_menu) goto_catalog end end rescue error("Error while parsing file.\n#{$!} (#{$!.class})\n" + $!.backtrace.join("\n")) end close_progressbar self.activate_focus end def deserialize dialog = Gtk::FileChooserDialog.new("Open dump file", self, FileChooser::ACTION_OPEN, nil, [Stock::CANCEL, Dialog::RESPONSE_CANCEL], [Stock::OPEN, Dialog::RESPONSE_ACCEPT] ) dialog.current_folder = File.join(Dir.pwd, "dumps") dialog.filter = FileFilter.new.add_pattern("*.gz") if dialog.run == Gtk::Dialog::RESPONSE_ACCEPT close if @opened filename = dialog.filename begin @opened = Origami::PDF.deserialize(filename) self.reload [ @file_menu_close, @file_menu_saveas, @file_menu_serialize, @file_menu_refresh, @document_menu_search, @document_menu_gotocatalog, @document_menu_gotopage, @document_menu_gotorev, @document_menu_gotoobj, @document_menu_properties, @document_menu_sign, @document_menu_ur ].each do |menu| menu.sensitive = true end @document_menu_gotodocinfo.sensitive = true if @opened.document_info? @document_menu_gotometadata.sensitive = true if @opened.metadata? @document_menu_gotofield.sensitive = true if @opened.form? @explorer_history.clear @statusbar.push(@main_context, "Viewing dump of #{filename}") rescue error("This file cannot be loaded.\n#{$!} (#{$!.class})") end end dialog.destroy end def serialize dialog = Gtk::FileChooserDialog.new("Save dump file", self, Gtk::FileChooser::ACTION_SAVE, nil, [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL], [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT] ) dialog.do_overwrite_confirmation = true dialog.current_folder = File.join(Dir.pwd, "dumps") dialog.current_name = "#{File.basename(@filename)}.dmp.gz" dialog.filter = FileFilter.new.add_pattern("*.gz") if dialog.run == Gtk::Dialog::RESPONSE_ACCEPT begin @opened.serialize(dialog.filename) rescue error("Error: #{$!.message}") end end dialog.destroy end def save_data(caption, data, filename = "") dialog = Gtk::FileChooserDialog.new(caption, self, Gtk::FileChooser::ACTION_SAVE, nil, [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL], [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT] ) dialog.do_overwrite_confirmation = true dialog.current_name = File.basename(filename) dialog.filter = FileFilter.new.add_pattern("*.*") if dialog.run == Gtk::Dialog::RESPONSE_ACCEPT begin File.binwrite(dialog.filename, data) rescue error("Error: #{$!.message}") end end dialog.destroy end def save dialog = Gtk::FileChooserDialog.new("Save PDF file", self, Gtk::FileChooser::ACTION_SAVE, nil, [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL], [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT] ) dialog.filter = FileFilter.new.add_pattern("*.acrodata").add_pattern("*.pdf").add_pattern("*.fdf") folder = File.dirname(@filename) dialog.set_current_folder(folder) if dialog.run == Gtk::Dialog::RESPONSE_ACCEPT begin @opened.save(dialog.filename) rescue error("#{$!.class}: #{$!.message}\n#{$!.backtrace.join($/)}") end end dialog.destroy end def save_dot dialog = Gtk::FileChooserDialog.new("Save dot file", self, Gtk::FileChooser::ACTION_SAVE, nil, [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL], [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT] ) dialog.filter = FileFilter.new.add_pattern("*.dot") folder = File.dirname(@filename) dialog.set_current_folder(folder) if dialog.run == Gtk::Dialog::RESPONSE_ACCEPT begin @opened.export_to_graph(dialog.filename) rescue error("Error: #{$!.message}") end end dialog.destroy end def save_graphml dialog = Gtk::FileChooserDialog.new("Save GraphML file", self, Gtk::FileChooser::ACTION_SAVE, nil, [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL], [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT] ) dialog.filter = FileFilter.new.add_pattern("*.graphml") folder = File.dirname(@filename) dialog.set_current_folder(folder) if dialog.run == Gtk::Dialog::RESPONSE_ACCEPT begin @opened.export_to_graphml(dialog.filename) rescue error("Error: #{$!.message}") end end dialog.destroy end private def parse_file(path) update_bar = lambda { |obj| @progressbar.pulse if @progressbar Gtk.main_iteration while Gtk.events_pending? } prompt_passwd = lambda { passwd = "" dialog = Gtk::Dialog.new( "This document is encrypted", nil, Gtk::Dialog::MODAL, [ Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK ], [ Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL ] ) dialog.set_default_response(Gtk::Dialog::RESPONSE_OK) label = Gtk::Label.new("Please enter password:") entry = Gtk::Entry.new entry.signal_connect('activate') { dialog.response(Gtk::Dialog::RESPONSE_OK) } dialog.vbox.add(label) dialog.vbox.add(entry) dialog.show_all dialog.run do |response| if response == Gtk::Dialog::RESPONSE_OK passwd = entry.text end end dialog.destroy return passwd } # # Try to detect the file type of the document. # Fallback to PDF if none is found. # file_type = detect_file_type(path) if file_type.nil? file_type = Origami::PDF force_mode = true else force_mode = false end file_type.read(path, verbosity: Origami::Parser::VERBOSE_TRACE, ignore_errors: false, callback: update_bar, prompt_password: prompt_passwd, force: force_mode ) end def detect_file_type(path) supported_types = [ Origami::PDF, Origami::FDF, Origami::PPKLite ] File.open(path, 'rb') do |file| data = file.read(128) supported_types.each do |type| return type if data.match(type::Header::MAGIC) end end nil end def create_progressbar @progresswin = Dialog.new("Parsing file...", self, Dialog::MODAL) @progresswin.vbox.add(@progressbar = ProgressBar.new.set_pulse_step(0.05)) @progresswin.show_all end def close_progressbar @progresswin.close end end end origami-2.0.0/bin/gui/config.rb0000644000004100000410000000661412757133666016345 0ustar www-datawww-data=begin This file is part of PDF Walker, a graphical PDF file browser Copyright (C) 2016 Guillaume Delugré. PDF Walker 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. PDF Walker 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 PDF Walker. If not, see . =end require 'origami' require 'yaml' module PDFWalker class Walker < Window class Config DEFAULT_CONFIG_FILE = "#{File.expand_path("~")}/.pdfwalker.conf.yml" DEFAULT_CONFIG = { "Debug" => { "Profiling" => false, "ProfilingOutputDir" => "prof", "Verbosity" => Origami::Parser::VERBOSE_TRACE, "IgnoreFileHeader" => true }, "UI" => { "LastOpenedDocuments" => [] } } NLOG_RECENT_FILES = 5 def initialize(configfile = DEFAULT_CONFIG_FILE) begin @conf = YAML.load(File.open(configfile)) or DEFAULT_CONFIG rescue ensure @filename = configfile set_missing_values end end def last_opened_file(filepath) @conf["UI"]['LastOpenedDocuments'].push(filepath).uniq! @conf["UI"]['LastOpenedDocuments'].delete_at(0) while @conf["UI"]['LastOpenedDocuments'].size > NLOG_RECENT_FILES save end def recent_files(n = NLOG_RECENT_FILES) @conf["UI"]['LastOpenedDocuments'].last(n).reverse end def set_profiling(bool) @conf["Debug"]['Profiling'] = bool save end def profile? @conf["Debug"]['Profiling'] end def profile_output_dir @conf["Debug"]['ProfilingOutputDir'] end def set_ignore_header(bool) @conf["Debug"]['IgnoreFileHeader'] = bool save end def ignore_header? @conf["Debug"]['IgnoreFileHeader'] end def set_verbosity(level) @conf["Debug"]['Verbosity'] = level save end def verbosity @conf["Debug"]['Verbosity'] end def save File.open(@filename, "w").write(@conf.to_yaml) end private def set_missing_values @conf ||= {} DEFAULT_CONFIG.each_key do |cat| @conf[cat] = {} unless @conf.include?(cat) DEFAULT_CONFIG[cat].each_pair do |key, value| @conf[cat][key] = value unless @conf[cat].include?(key) end end end end end end origami-2.0.0/bin/gui/hexview.rb0000644000004100000410000000436512757133666016560 0ustar www-datawww-data=begin This file is part of PDF Walker, a graphical PDF file browser Copyright (C) 2016 Guillaume Delugré. PDF Walker 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. PDF Walker 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 PDF Walker. If not, see . =end require 'gui/gtkhex' module PDFWalker class Walker < Window private def create_hexview @hexview = DumpView.new(self) end class DumpView < ScrolledWindow def initialize(parent) @parent = parent super() set_policy(POLICY_AUTOMATIC, POLICY_AUTOMATIC) @current_obj = nil @view = HexEditor.new @view.show_offsets(true) add_with_viewport @view end def clear @view.set_data '' end def load(object) return if @current_obj.equal?(object) begin self.clear case object when Origami::Stream begin @view.set_data(object.data) rescue Origami::Filter::Error @view.set_data($!.input_data) if $!.input_data @parent.error("#{$!.class}: #{$!.message}") unless object.filters == [ :DCTDecode ] end when Origami::String @view.set_data(object.value) end @current_obj = object rescue @parent.error("An error occured while loading this object.\n#{$!} (#{$!.class})") end end end end end origami-2.0.0/bin/gui/walker.rb0000644000004100000410000002042412757133666016360 0ustar www-datawww-data#!/usr/bin/env ruby =begin This file is part of PDF Walker, a graphical PDF file browser Copyright (C) 2016 Guillaume Delugré. PDF Walker 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. PDF Walker 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 PDF Walker. If not, see . =end begin require 'gtk2' rescue LoadError abort('Error: you need to install ruby-gtk2 to run this application') end include Gtk begin require 'origami' rescue LoadError $: << File.join(__dir__, '../../lib') require 'origami' end require 'gui/menu' require 'gui/about' require 'gui/file' require 'gui/hexview' require 'gui/treeview' require 'gui/textview' require 'gui/imgview' require 'gui/config' require 'gui/properties' require 'gui/xrefs' require 'gui/signing' module PDFWalker #:nodoc:all class Walker < Window attr_reader :treeview, :hexview, :objectview attr_reader :explorer_history attr_reader :config attr_reader :filename def self.start(file = nil) Gtk.init Walker.new(file) Gtk.main end def initialize(target_file = nil) super("PDF Walker") @config = Walker::Config.new @opened = nil @last_search_result = [] @last_search = { :expr => "", :regexp => false, :type => :body } @explorer_history = Array.new signal_connect('destroy') { @config.save Gtk.main_quit } add_events(Gdk::Event::KEY_RELEASE_MASK) signal_connect('key_release_event') { |w, event| if event.keyval == Gdk::Keyval::GDK_F1 then about elsif event.keyval == Gdk::Keyval::GDK_Escape and @opened and not @explorer_history.empty? @treeview.goto(@explorer_history.pop) end } create_menus create_treeview create_hexview create_objectview create_panels create_statusbar @vbox = VBox.new @vbox.pack_start(@menu, false, false) @vbox.pack_start(@hpaned) @vbox.pack_end(@statusbar, false, false) add @vbox set_default_size(self.screen.width * 0.5, self.screen.height * 0.5) #maximize show_all open(target_file) end def error(msg) dialog = Gtk::MessageDialog.new(self, Gtk::Dialog::DESTROY_WITH_PARENT, Gtk::MessageDialog::ERROR, Gtk::MessageDialog::BUTTONS_CLOSE, msg ) dialog.run dialog.destroy end def reload @treeview.load(@opened) if @opened end def search dialog = Gtk::Dialog.new("Search...", self, Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT, [Gtk::Stock::FIND, Gtk::Dialog::RESPONSE_OK], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL] ) entry = Gtk::Entry.new entry.signal_connect('activate') { dialog.response(Gtk::Dialog::RESPONSE_OK) } entry.text = @last_search[:expr] button_bydata = Gtk::RadioButton.new("In object body") button_byname = Gtk::RadioButton.new(button_bydata, "In object name") button_regexp = Gtk::CheckButton.new("Regular expression") button_bydata.set_active(true) if @last_search[:type] == :body button_byname.set_active(true) if @last_search[:type] == :name button_regexp.set_active(@last_search[:regexp]) hbox = HBox.new hbox.pack_start Gtk::Label.new("Search for expression ") hbox.pack_start entry dialog.vbox.pack_start(hbox) dialog.vbox.pack_start(button_bydata) dialog.vbox.pack_start(button_byname) dialog.vbox.pack_end(button_regexp) dialog.signal_connect('response') do |dlg, response| if response != Gtk::Dialog::RESPONSE_OK dialog.destroy next end search = { :expr => entry.text, :regexp => button_regexp.active?, :type => button_byname.active? ? :name : :body } if search == @last_search @last_search_result.push @last_search_result.shift results = @last_search_result else expr = search[:regexp] ? Regexp.new(search[:expr]) : search[:expr] results = if search[:type] == :body @opened.grep(expr) else @opened.ls(expr, follow_references: false) end @last_search = search end if results.empty? error("No result found.") else if results != @last_search_result # Reset the previous search highlighting. @last_search_result.each do |obj| @treeview.highlight(obj, nil) end # Highlight the new results. results.each do |obj| @treeview.highlight(obj, "lightpink") end @last_search_result = results end @treeview.goto(results.first, follow_references: false) end end dialog.show_all end def goto_catalog @treeview.goto(@opened.Catalog.reference) end def goto_docinfo @treeview.goto(@opened.document_info.reference) if @opened.document_info? end def goto_metadata @treeview.goto(@opened.Catalog.Metadata.reference) if @opened.metadata? end def goto_object dialog = Gtk::Dialog.new("Jump to object...", self, Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT, [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL] ) entry = Gtk::Entry.new entry.signal_connect('activate') { dialog.response(Gtk::Dialog::RESPONSE_OK) } dialog.vbox.pack_start Gtk::Label.new("Object number: ") dialog.vbox.pack_start entry dialog.show_all no = 0 dialog.run do |response| no = entry.text.to_i if response == Gtk::Dialog::RESPONSE_OK dialog.destroy end return unless no > 0 obj = @opened[no] if obj.nil? error("Object #{no} not found.") else @treeview.goto(obj) end end private def create_panels @hpaned = HPaned.new @treepanel = ScrolledWindow.new.set_policy(POLICY_AUTOMATIC, POLICY_AUTOMATIC) @treepanel.add @treeview @vpaned = VPaned.new @vpaned.pack1(@objectview, true, false) @vpaned.pack2(@hexview, true, false) @hpaned.pack1(@treepanel, true, false) @hpaned.pack2(@vpaned, true, false) end def create_statusbar @statusbar = Statusbar.new @main_context = @statusbar.get_context_id 'Main' @statusbar.push(@main_context, 'No file selected') end end end PDFWalker::Walker.start if __FILE__ == $0 origami-2.0.0/bin/gui/xrefs.rb0000644000004100000410000000463512757133666016230 0ustar www-datawww-data=begin This file is part of PDF Walker, a graphical PDF file browser Copyright (C) 2016 Guillaume Delugré. PDF Walker 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. PDF Walker 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 PDF Walker. If not, see . =end module PDFWalker class Walker < Window def show_xrefs(target) XrefsDialog.new(self, target) end end class XrefsDialog < Dialog OBJCOL = 0 REFCOL = 1 NAMECOL = 2 def initialize(parent, target) super("Xrefs to #{target.reference}", parent, Dialog::MODAL, [Stock::CLOSE, Dialog::RESPONSE_NONE]) @parent = parent @list = ListStore.new(Object, String, String) @view = TreeView.new(@list) column = Gtk::TreeViewColumn.new("Origin", Gtk::CellRendererText.new, text: REFCOL) @view.append_column(column) column = Gtk::TreeViewColumn.new("Objects", Gtk::CellRendererText.new, text: NAMECOL) @view.append_column(column) target.xrefs.each { |obj| str = obj.type.to_s iter = @list.append @list.set_value(iter, OBJCOL, obj) obj = obj.parent until obj.indirect? @list.set_value(iter, REFCOL, obj.reference.to_s) @list.set_value(iter, NAMECOL, str) } @view.signal_connect("row_activated") { |tree, path, _column| if @view.selection.selected from = @list.get_value(@view.selection.selected, OBJCOL) @parent.treeview.goto(from) end } scroll = ScrolledWindow.new.set_policy(POLICY_NEVER, POLICY_AUTOMATIC) scroll.add(@view) vbox.add(scroll) set_default_size(200, 200) signal_connect('response') { destroy } show_all end end end origami-2.0.0/bin/gui/menu.rb0000644000004100000410000004060512757133666016042 0ustar www-datawww-data=begin This file is part of PDF Walker, a graphical PDF file browser Copyright (C) 2016 Guillaume Delugré. PDF Walker 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. PDF Walker 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 PDF Walker. If not, see . =end module PDFWalker module Popable @@menus = Hash.new([]) @@menus[:"PDF File"] = [ { Name: Stock::SAVE_AS, Sensitive: true, Callback: lambda { |widget, viewer, path| viewer.parent.save } }, { Name: "Serialize", Sensitive: true, Callback: lambda { |widget, viewer, path| viewer.parent.serialize } }, { Name: :"---" }, { Name: Stock::PROPERTIES, Sensitive: true, Callback: lambda { |widget, viewer, path| viewer.parent.display_file_properties } }, { Name: :"---" }, { Name: Stock::CLOSE, Sensitive: true, Callback: lambda { |widget, viewer, path| viewer.parent.close } } ] @@menus[:Reference] = [ { Name: Stock::JUMP_TO, Sensitive: true, Callback: lambda { |widget, viewer, path| viewer.row_activated(path, viewer.get_column(viewer.class::TEXTCOL)) } } ] @@menus[:Revision] = [ { Name: "Save to this revision", Sensitive: true, Callback: lambda { |widget, viewer, path| revstr = viewer.model.get_value(viewer.model.get_iter(path), viewer.class::TEXTCOL) revstr.slice!(0, "Revision ".size) revnum = revstr.to_i dialog = Gtk::FileChooserDialog.new("Save PDF File", viewer.parent, Gtk::FileChooser::ACTION_SAVE, nil, [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL], [Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT] ) dialog.filter = FileFilter.new.add_pattern("*.pdf") if dialog.run == Gtk::Dialog::RESPONSE_ACCEPT viewer.parent.opened.save_upto(revnum, dialog.filename) end dialog.destroy } } ] @@menus[:Stream] = [ { Name: "Dump encoded stream", Sensitive: true, Callback: lambda { |widget, viewer, path| stream = viewer.model.get_value(viewer.model.get_iter(path), viewer.class::OBJCOL) viewer.parent.save_data("Save stream to file", stream.encoded_data) } }, { Name: "Dump decoded stream", Sensitive: true, Callback: lambda { |widget, viewer, path| stream = viewer.model.get_value(viewer.model.get_iter(path), viewer.class::OBJCOL) viewer.parent.save_data("Save stream to file", stream.data) } } ] @@menus[:String] = [ { Name: "Dump string", Sensitive: true, Callback: lambda { |widget, viewer, path| string = viewer.model.get_value(viewer.model.get_iter(path), viewer.class::OBJCOL) viewer.parent.save_data("Save string to file", string.value) } } ] @@menus[:Image] = @@menus[:Stream] + [ { Name: :"---" }, { Name: "View image", Sensitive: true, Callback: lambda { |widget, viewer, path| stm = viewer.model.get_value(viewer.model.get_iter(path), viewer.class::OBJCOL) w,h = stm.Width, stm.Height if stm.ColorSpace.nil? colors = 1 else colors = case stm.ColorSpace.value when :DeviceGray then 1 when :DeviceRGB then 3 when :DeviceCMYK then 4 else 1 end end bpc = stm.BitsPerComponent || 8 bpr = (w * colors * bpc + 7) >> 3 data = stm.data begin imgview = ImgViewer.new if stm.Filter == :DCTDecode or (stm.Filter.is_a?(Array) and stm.Filter[0] == :DCTDecode) imgview.show_compressed_img data else imgview.show_raw_img data, w, h, bpc, bpr end rescue viewer.parent.error("#{$!.class}: #{$!.message}") end } } ] def popup_menu(obj, event, path) menu = Menu.new type = if obj.is_a?(Origami::Object) if obj.is_a?(Origami::Graphics::ImageXObject) :Image else obj.native_type.to_s.split("::").last.to_sym end else case obj when Origami::PDF :"PDF File" when Origami::PDF::Revision, Origami::FDF::Revision, Origami::PPKLite::Revision :Revision when ::Array :Body when Origami::PDF::Header, Origami::FDF::Header, Origami::PPKLite::Header :Header when Origami::Trailer :Trailer when Origami::XRef::Section :XRefSection when Origami::XRef::Subsection :XRefSubsection when Origami::XRef, Origami::XRefToCompressedObj :XRef else :Unknown end end title = obj.is_a?(Origami::Object) ? "Object : " : "" title << type.to_s menu.append(MenuItem.new(title).set_sensitive(false).modify_text(Gtk::STATE_INSENSITIVE, Gdk::Color.new(255,0,255))) if obj.is_a?(Origami::Object) if obj.indirect? menu.append(MenuItem.new("Number : #{obj.no}; Generation : #{obj.generation}").set_sensitive(false)) menu.append(MenuItem.new("File offset : #{obj.file_offset}").set_sensitive(false)) getxrefs = MenuItem.new("Search references to this object").set_sensitive(true) getxrefs.signal_connect("activate") do ref = self.model.get_value(self.model.get_iter(path), self.class::OBJCOL) self.parent.show_xrefs(ref) end menu.append(getxrefs) elsif not obj.parent.nil? gotoparent = MenuItem.new("Goto Parent Object").set_sensitive(true) gotoparent.signal_connect("activate") do dest = self.model.get_value(self.model.get_iter(path), self.class::OBJCOL).parent self.goto(dest) end menu.append(gotoparent) end end items = @@menus[type] menu.append(SeparatorMenuItem.new) if not items.empty? items.each { |item| if item[:Name] == :"---" entry = SeparatorMenuItem.new else if item[:Name].is_a?(String) entry = MenuItem.new(item[:Name]) else entry = ImageMenuItem.new(item[:Name]) end entry.set_sensitive(item[:Sensitive]) entry.signal_connect("activate", self, path, &item[:Callback]) end menu.append(entry) } menu.show_all menu.popup(nil, nil, event.button, event.time) end end class Walker < Window private def create_menus AccelMap.add_entry("/File/Open", Gdk::Keyval::GDK_O, Gdk::Window::CONTROL_MASK) AccelMap.add_entry("/File/Refresh", Gdk::Keyval::GDK_R, Gdk::Window::CONTROL_MASK) AccelMap.add_entry("/File/Close", Gdk::Keyval::GDK_W, Gdk::Window::CONTROL_MASK) AccelMap.add_entry("/File/Save", Gdk::Keyval::GDK_S, Gdk::Window::CONTROL_MASK) AccelMap.add_entry("/File/Quit", Gdk::Keyval::GDK_Q, Gdk::Window::CONTROL_MASK) AccelMap.add_entry("/Document/Search", Gdk::Keyval::GDK_F, Gdk::Window::CONTROL_MASK) @menu = MenuBar.new #################################################### file_ag = Gtk::AccelGroup.new @file_menu = Menu.new.set_accel_group(file_ag).set_accel_path("/File") add_accel_group(file_ag) @file_menu_open = ImageMenuItem.new(Stock::OPEN).set_accel_path("/File/Open") @file_menu_recent = MenuItem.new("Last opened") @file_menu_deserialize = MenuItem.new("Deserialize") @file_menu_refresh = ImageMenuItem.new(Stock::REFRESH).set_sensitive(false).set_accel_path("/File/Refresh") @file_menu_close = ImageMenuItem.new(Stock::CLOSE).set_sensitive(false).set_accel_path("/File/Close") @file_menu_saveas = ImageMenuItem.new(Stock::SAVE_AS).set_sensitive(false) @file_menu_serialize = MenuItem.new("Serialize").set_sensitive(false) @file_menu_exit = ImageMenuItem.new(Stock::QUIT).set_accel_path("/File/Quit") @export_menu = Menu.new @export_pdf_menu = MenuItem.new("As PDF").set_accel_path("/File/Save") @export_graph_menu = MenuItem.new("As GraphViz dot file") @export_graphml_menu = MenuItem.new("As GraphML file") @export_pdf_menu.signal_connect('activate') do save end @export_graph_menu.signal_connect('activate') do save_dot end @export_graphml_menu.signal_connect('activate') do save_graphml end @export_menu.append(@export_pdf_menu) @export_menu.append(@export_graph_menu) @export_menu.append(@export_graphml_menu) @file_menu_saveas.set_submenu(@export_menu) @file_menu_open.signal_connect('activate') do open end @file_menu_deserialize.signal_connect('activate') do deserialize end @file_menu_refresh.signal_connect('activate') do open(@filename) end @file_menu_close.signal_connect('activate') do close end @file_menu_serialize.signal_connect('activate') do serialize end @file_menu_exit.signal_connect('activate') do self.destroy end update_recent_menu @file_menu.append(@file_menu_open) @file_menu.append(@file_menu_recent) @file_menu.append(@file_menu_deserialize) @file_menu.append(@file_menu_refresh) @file_menu.append(@file_menu_close) @file_menu.append(@file_menu_saveas) @file_menu.append(@file_menu_serialize) @file_menu.append(@file_menu_exit) @menu.append(MenuItem.new('_File').set_submenu(@file_menu)) #################################################### doc_ag = Gtk::AccelGroup.new @document_menu = Menu.new.set_accel_group(doc_ag) add_accel_group(doc_ag) @document_menu_search = ImageMenuItem.new(Stock::FIND).set_sensitive(false).set_accel_path("/Document/Search") @document_menu_gotocatalog = MenuItem.new("Jump to Catalog").set_sensitive(false) @document_menu_gotodocinfo = MenuItem.new("Jump to Document Info").set_sensitive(false) @document_menu_gotometadata = MenuItem.new("Jump to Metadata").set_sensitive(false) @document_menu_gotorev = MenuItem.new("Jump to Revision...").set_sensitive(false) @document_menu_gotopage = MenuItem.new("Jump to Page...").set_sensitive(false) @document_menu_gotofield = MenuItem.new("Jump to Field...").set_sensitive(false) @document_menu_gotoobj = MenuItem.new("Jump to Object...").set_sensitive(false) @document_menu_properties = ImageMenuItem.new(Stock::PROPERTIES).set_sensitive(false) @document_menu_sign = MenuItem.new("Sign the document").set_sensitive(false) @document_menu_ur = MenuItem.new("Enable Usage Rights").set_sensitive(false) @document_menu_search.signal_connect('activate') do search end @document_menu_gotocatalog.signal_connect('activate') do goto_catalog end @document_menu_gotodocinfo.signal_connect('activate') do goto_docinfo end @document_menu_gotometadata.signal_connect('activate') do goto_metadata end @document_menu_gotoobj.signal_connect('activate') do goto_object end @document_menu_properties.signal_connect('activate') do display_file_properties end @document_menu_sign.signal_connect('activate') do display_signing_wizard end @document_menu_ur.signal_connect('activate') do display_usage_rights_wizard end @document_menu.append(@document_menu_search) @document_menu.append(MenuItem.new) @document_menu.append(@document_menu_gotocatalog) @document_menu.append(@document_menu_gotodocinfo) @document_menu.append(@document_menu_gotometadata) @document_menu.append(@document_menu_gotorev) @document_menu.append(@document_menu_gotopage) @document_menu.append(@document_menu_gotofield) @document_menu.append(@document_menu_gotoobj) @document_menu.append(MenuItem.new) @document_menu.append(@document_menu_sign) @document_menu.append(@document_menu_ur) @document_menu.append(@document_menu_properties) @menu.append(MenuItem.new('_Document').set_submenu(@document_menu)) #################################################### @help_menu = Menu.new @help_menu_profile = CheckMenuItem.new("Profiling (Debug purposes only)").set_active(@config.profile?) @help_menu_profile.signal_connect('toggled') do @config.set_profiling(@help_menu_profile.active?) end @help_menu_about = ImageMenuItem.new(Stock::ABOUT) @help_menu_about.signal_connect('activate') do about end @help_menu.append(@help_menu_profile) @help_menu.append(@help_menu_about) @menu.append(MenuItem.new('_Help').set_submenu(@help_menu)) #################################################### end def update_recent_menu @recent_menu = Menu.new @config.recent_files.each { |file| menu = MenuItem.new(file) menu.signal_connect('activate') do open(file) end @recent_menu.append(menu) } @file_menu_recent.set_submenu(@recent_menu) @file_menu_recent.show_all end end end origami-2.0.0/bin/gui/signing.rb0000644000004100000410000005710412757133666016536 0ustar www-datawww-data=begin This file is part of PDF Walker, a graphical PDF file browser Copyright (C) 2016 Guillaume Delugré. PDF Walker 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. PDF Walker 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 PDF Walker. If not, see . =end module PDFWalker class Walker < Window def display_signing_wizard SignWizard.new(self, @opened) if @opened end def display_usage_rights_wizard UsageRightsWizard.new(self, @opened) if @opened end module SignatureDialogs private def open_private_key_dialog(page) dialog = FileChooserDialog.new("Choose a private RSA key", @parent, FileChooser::ACTION_OPEN, nil, [Stock::CANCEL, Dialog::RESPONSE_CANCEL], [Stock::OPEN, Dialog::RESPONSE_ACCEPT]) filter = FileFilter.new filter.add_pattern("*.key") filter.add_pattern("*.pem") filter.add_pattern("*.der") dialog.set_filter(filter) if dialog.run == Dialog::RESPONSE_ACCEPT begin @pkey = OpenSSL::PKey::RSA.new(File.binread(dialog.filename)) @pkeyfilename.set_text(dialog.filename) set_page_complete(page, true) if @cert rescue @parent.error("Error loading file '#{File.basename(dialog.filename)}'") @pkey = nil @pkeyfilename.text = "" set_page_complete(page, false) ensure @ca = [] # Shall be added to the GUI end end dialog.destroy end def open_certificate_dialog(page) dialog = FileChooserDialog.new("Choose a x509 certificate", @parent, FileChooser::ACTION_OPEN, nil, [Stock::CANCEL, Dialog::RESPONSE_CANCEL], [Stock::OPEN, Dialog::RESPONSE_ACCEPT]) filter = FileFilter.new filter.add_pattern("*.crt") filter.add_pattern("*.cer") filter.add_pattern("*.pem") filter.add_pattern("*.der") dialog.set_filter(filter) if dialog.run == Dialog::RESPONSE_ACCEPT begin @cert = OpenSSL::X509::Certificate.new(File.binread(dialog.filename)) @certfilename.set_text(dialog.filename) if @pkey then set_page_complete(page, true) end rescue @parent.error("Error loading file '#{File.basename(dialog.filename)}'") @cert = nil @certfilename.text = "" set_page_complete(page, false) ensure @ca = [] # Shall be added to the GUI end end dialog.destroy end def open_pkcs12_file_dialog(page) get_passwd = -> () do dialog = Dialog.new("Enter passphrase", @parent, Dialog::MODAL, [Stock::OK, Dialog::RESPONSE_OK] ) pwd_entry = Entry.new.set_visibility(false).show dialog.vbox.pack_start(pwd_entry, true, true, 0) pwd = (dialog.run == Dialog::RESPONSE_OK) ? pwd_entry.text : "" dialog.destroy return pwd end dialog = FileChooserDialog.new("Open PKCS12 container", @parent, FileChooser::ACTION_OPEN, nil, [Stock::CANCEL, Dialog::RESPONSE_CANCEL], [Stock::OPEN, Dialog::RESPONSE_ACCEPT]) filter = FileFilter.new filter.add_pattern("*.pfx") filter.add_pattern("*.p12") dialog.filter = filter if dialog.run == Dialog::RESPONSE_ACCEPT begin p12 = OpenSSL::PKCS12::PKCS12.new(File.binread(dialog.filename), get_passwd) raise TypeError, "PKCS12 does not contain a RSA key" unless p12.key.is_a?(OpenSSL::PKey::RSA) raise TypeError, "PKCS12 does not contain a x509 certificate" unless p12.certificate.is_a?(OpenSSL::X509::Certificate) @pkey = p12.key @cert = p12.certificate @ca = p12.ca_certs @p12filename.set_text(dialog.filename) set_page_complete(page, true) rescue @parent.error("Error loading file '#{File.basename(dialog.filename)}'") @pkey, @cert, @ca = nil, nil, [] @p12filename.text = "" set_page_complete(page, false) end end dialog.destroy end end class UsageRightsWizard < Assistant include SignatureDialogs def initialize(parent, pdf) super() @parent = parent @pkey, @cert = nil, nil create_intro_page create_keypair_import_page create_rights_selection_page create_termination_page signal_connect('delete_event') { self.destroy } signal_connect('cancel') { self.destroy } signal_connect('close') { self.destroy } signal_connect('apply') { rights = [] rights << Origami::UsageRights::Rights::DOCUMENT_FULLSAVE if @document_fullsave.active? rights << Origami::UsageRights::Rights::ANNOTS_CREATE if @annots_create.active? rights << Origami::UsageRights::Rights::ANNOTS_DELETE if @annots_delete.active? rights << Origami::UsageRights::Rights::ANNOTS_MODIFY if @annots_modify.active? rights << Origami::UsageRights::Rights::ANNOTS_COPY if @annots_copy.active? rights << Origami::UsageRights::Rights::ANNOTS_IMPORT if @annots_import.active? rights << Origami::UsageRights::Rights::ANNOTS_EXPORT if @annots_export.active? rights << Origami::UsageRights::Rights::ANNOTS_ONLINE if @annots_online.active? rights << Origami::UsageRights::Rights::ANNOTS_SUMMARYVIEW if @annots_sumview.active? rights << Origami::UsageRights::Rights::FORM_FILLIN if @form_fillin.active? rights << Origami::UsageRights::Rights::FORM_IMPORT if @form_import.active? rights << Origami::UsageRights::Rights::FORM_EXPORT if @form_export.active? rights << Origami::UsageRights::Rights::FORM_SUBMITSTANDALONE if @form_submit.active? rights << Origami::UsageRights::Rights::FORM_SPAWNTEMPLATE if @form_spawntemplate.active? rights << Origami::UsageRights::Rights::FORM_BARCODEPLAINTEXT if @form_barcode.active? rights << Origami::UsageRights::Rights::FORM_ONLINE if @form_online.active? rights << Origami::UsageRights::Rights::SIGNATURE_MODIFY if @signature_modify.active? rights << Origami::UsageRights::Rights::EF_CREATE if @ef_create.active? rights << Origami::UsageRights::Rights::EF_DELETE if @ef_delete.active? rights << Origami::UsageRights::Rights::EF_MODIFY if @ef_modify.active? rights << Origami::UsageRights::Rights::EF_IMPORT if @ef_import.active? begin pdf.enable_usage_rights(@cert, @pkey, *rights) set_page_title(@lastpage, "Usage Rights have been enabled") @msg_status.text = "Usage Rights have been enabled for the current document.\n You should consider saving it now." @parent.reload rescue @parent.error("#{$!}: #{$!.backtrace.join($/)}") set_page_title(@lastpage, "Usage Rights have not been enabled") @msg_status.text = "An error occured during the signature process." end } set_modal(true) show_all end private def create_intro_page intro = <<-INTRO You are about to enable Usage Rights for the current PDF document. To enable these features, you need to have an Adobe public/private key pair in your possession. Make sure you have adobe.crt and adobe.key located in the current directory. INTRO vbox = VBox.new(false, 5) vbox.set_border_width(5) lbl = Label.new(intro).set_justify(Gtk::JUSTIFY_LEFT).set_wrap(true) vbox.pack_start(lbl, true, true, 0) append_page(vbox) set_page_title(vbox, "Usage Rights Wizard") set_page_type(vbox, Assistant::PAGE_INTRO) set_page_complete(vbox, true) end def create_keypair_import_page labels = [ [ "Private RSA key:", @pkeyfilename = Entry.new, pkeychoosebtn = Button.new(Gtk::Stock::OPEN) ], [ "Public certificate:", @certfilename = Entry.new, certchoosebtn = Button.new(Gtk::Stock::OPEN) ] ] row = 0 table = Table.new(2, 3) labels.each do |lbl, entry, btn| entry.editable = entry.sensitive = false table.attach(Label.new(lbl).set_alignment(1,0), 0, 1, row, row + 1, Gtk::EXPAND | Gtk::FILL, Gtk::SHRINK, 4, 4) table.attach(entry, 1, 2, row, row + 1, Gtk::EXPAND | Gtk::FILL, Gtk::SHRINK, 4, 4) table.attach(btn, 2, 3, row, row + 1, Gtk::EXPAND | Gtk::FILL, Gtk::SHRINK, 4, 4) row = row.succ end pkeychoosebtn.signal_connect('clicked') { open_private_key_dialog(table) } certchoosebtn.signal_connect('clicked') { open_certificate_dialog(table) } append_page(table) set_page_title(table, "Import a public/private key pair") set_page_type(table, Assistant::PAGE_CONTENT) end def create_rights_selection_page vbox = VBox.new(false, 5) docframe = Frame.new(" Document ") docframe.border_width = 5 docframe.shadow_type = Gtk::SHADOW_IN doctable = Table.new(1, 2) doctable.attach(@document_fullsave = CheckButton.new("Full Save").set_active(true), 0, 1, 0, 1, Gtk::EXPAND | Gtk::FILL, Gtk::SHRINK, 4, 4) docframe.add(doctable) annotsframe = Frame.new(" Annotations ") annotsframe.border_width = 5 annotsframe.shadow_type = Gtk::SHADOW_IN annotstable = Table.new(4,2) annots = [ [ @annots_create = CheckButton.new("Create").set_active(true), @annots_import = CheckButton.new("Import").set_active(true) ], [ @annots_delete = CheckButton.new("Delete").set_active(true), @annots_export = CheckButton.new("Export").set_active(true) ], [ @annots_modify = CheckButton.new("Modify").set_active(true), @annots_online = CheckButton.new("Online").set_active(true) ], [ @annots_copy = CheckButton.new("Copy").set_active(true), @annots_sumview = CheckButton.new("Summary View").set_active(true) ] ] row = 0 annots.each do |col1, col2| annotstable.attach(col1, 0, 1, row, row + 1, Gtk::EXPAND | Gtk::FILL, Gtk::SHRINK, 4, 4) annotstable.attach(col2, 1, 2, row, row + 1, Gtk::EXPAND | Gtk::FILL, Gtk::SHRINK, 4, 4) row = row.succ end annotsframe.add(annotstable) formframe = Frame.new(" Forms ") formframe.border_width = 5 formframe.shadow_type = Gtk::SHADOW_IN formtable = Table.new(4,2) forms = [ [ @form_fillin = CheckButton.new("Fill in").set_active(true), @form_spawntemplate = CheckButton.new("Spawn template").set_active(true) ], [ @form_import = CheckButton.new("Import").set_active(true), @form_barcode = CheckButton.new("Barcode plaintext").set_active(true) ], [ @form_export = CheckButton.new("Export").set_active(true), @form_online = CheckButton.new("Online").set_active(true) ], [ @form_submit = CheckButton.new("Submit stand-alone").set_active(true), nil ] ] row = 0 forms.each do |col1, col2| formtable.attach(col1, 0, 1, row, row + 1, Gtk::EXPAND | Gtk::FILL, Gtk::SHRINK, 4, 4) formtable.attach(col2, 1, 2, row, row + 1, Gtk::EXPAND | Gtk::FILL, Gtk::SHRINK, 4, 4) unless col2.nil? row = row.succ end formframe.add(formtable) signatureframe = Frame.new(" Signature ") signatureframe.border_width = 5 signatureframe.shadow_type = Gtk::SHADOW_IN signaturetable = Table.new(1, 2) signaturetable.attach(@signature_modify = CheckButton.new("Modify").set_active(true), 0, 1, 0, 1, Gtk::EXPAND | Gtk::FILL, Gtk::SHRINK, 4, 4) signatureframe.add(signaturetable) efframe = Frame.new(" Embedded files ") efframe.border_width = 5 efframe.shadow_type = Gtk::SHADOW_IN eftable = Table.new(2,2) efitems = [ [ @ef_create = CheckButton.new("Create").set_active(true), @ef_modify = CheckButton.new("Modify").set_active(true) ], [ @ef_delete = CheckButton.new("Delete").set_active(true), @ef_import = CheckButton.new("Import").set_active(true) ] ] row = 0 efitems.each do |col1, col2| eftable.attach(col1, 0, 1, row, row + 1, Gtk::EXPAND | Gtk::FILL, Gtk::SHRINK, 4, 4) eftable.attach(col2, 1, 2, row, row + 1, Gtk::EXPAND | Gtk::FILL, Gtk::SHRINK, 4, 4) row = row.succ end efframe.add(eftable) vbox.add(docframe) vbox.add(annotsframe) vbox.add(formframe) vbox.add(signatureframe) vbox.add(efframe) append_page(vbox) set_page_title(vbox, "Select Usage Rights to enable") set_page_type(vbox, Assistant::PAGE_CONFIRM) set_page_complete(vbox, true) end def create_termination_page @lastpage = VBox.new(false, 5) @msg_status = Label.new @lastpage.pack_start(@msg_status, true, true, 0) append_page(@lastpage) set_page_title(@lastpage, "Usage Rights have not been enabled") set_page_type(@lastpage, Assistant::PAGE_SUMMARY) end end class SignWizard < Assistant include SignatureDialogs INTRO_PAGE = 0 KEY_SELECT_PAGE = 1 PKCS12_IMPORT_PAGE = 2 KEYPAIR_IMPORT_PAGE = 3 SIGNATURE_INFO_PAGE = 4 SIGNATURE_RESULT_PAGE = 5 def initialize(parent, pdf) super() @parent = parent @pkey, @cert, @ca = nil, nil, [] create_intro_page create_key_selection_page create_pkcs12_import_page create_keypair_import_page create_signature_info_page create_termination_page set_forward_page_func { |current_page| case current_page when KEY_SELECT_PAGE if @p12button.active? then PKCS12_IMPORT_PAGE else KEYPAIR_IMPORT_PAGE end when PKCS12_IMPORT_PAGE, KEYPAIR_IMPORT_PAGE SIGNATURE_INFO_PAGE else current_page.succ end } signal_connect('delete_event') { self.destroy } signal_connect('cancel') { self.destroy } signal_connect('close') { self.destroy } signal_connect('apply') { location = @location.text.empty? ? nil : @location.text contact = @email.text.empty? ? nil : @email.text reason = @reason.text.empty? ? nil : @reason.text begin pdf.sign(@cert, @pkey, ca: @ca, location: location, contact: contact, reason: reason) set_page_title(@lastpage, "Document has been signed") @msg_status.text = "The document has been signed.\n You should consider saving it now." @parent.reload rescue @parent.error("#{$!}: #{$!.backtrace.join($/)}") set_page_title(@lastpage, "Document has not been signed") @msg_status.text = "An error occured during the signature process." end } set_modal(true) show_all end private def create_intro_page intro = <<-INTRO You are about to sign the current PDF document. Once the document will be signed, no further modification will be allowed. The signature process is based on assymetric cryptography, so you will basically need a public/private RSA key pair (between 1024 and 4096 bits). INTRO vbox = VBox.new(false, 5) vbox.set_border_width(5) lbl = Label.new(intro).set_justify(Gtk::JUSTIFY_LEFT).set_wrap(true) vbox.pack_start(lbl, true, true, 0) append_page(vbox) set_page_title(vbox, "Signature Wizard") set_page_type(vbox, Assistant::PAGE_INTRO) set_page_complete(vbox, true) end def create_key_selection_page vbox = VBox.new(false, 5) @rawbutton = RadioButton.new("Import keys from separate PEM/DER encoded files") @p12button = RadioButton.new(@rawbutton, "Import keys from a PKCS12 container") vbox.pack_start(@rawbutton, true, true, 0) vbox.pack_start(@p12button, true, true, 0) append_page(vbox) set_page_title(vbox, "Choose a key importation method") set_page_type(vbox, Assistant::PAGE_CONTENT) set_page_complete(vbox, true) end def create_pkcs12_import_page vbox = VBox.new(false, 5) hbox = HBox.new(false, 5) vbox.pack_start(hbox, true, false, 10) @p12filename = Entry.new.set_editable(false).set_sensitive(false) choosebtn = Button.new(Gtk::Stock::OPEN) choosebtn.signal_connect('clicked') { open_pkcs12_file_dialog(vbox) } hbox.pack_start(@p12filename, true, true, 5) hbox.pack_start(choosebtn, false, false, 5) append_page(vbox) set_page_title(vbox, "Import a PKCS12 container") set_page_type(vbox, Assistant::PAGE_CONTENT) end def create_keypair_import_page labels = [ [ "Private RSA key:", @pkeyfilename = Entry.new, pkeychoosebtn = Button.new(Gtk::Stock::OPEN) ], [ "Public certificate:", @certfilename = Entry.new, certchoosebtn = Button.new(Gtk::Stock::OPEN) ] ] row = 0 table = Table.new(2, 3) labels.each do |lbl, entry, btn| entry.editable = entry.sensitive = false table.attach(Label.new(lbl).set_alignment(1,0), 0, 1, row, row + 1, Gtk::EXPAND | Gtk::FILL, Gtk::SHRINK, 4, 4) table.attach(entry, 1, 2, row, row + 1, Gtk::EXPAND | Gtk::FILL, Gtk::SHRINK, 4, 4) table.attach(btn, 2, 3, row, row + 1, Gtk::EXPAND | Gtk::FILL, Gtk::SHRINK, 4, 4) row = row.succ end pkeychoosebtn.signal_connect('clicked') { open_private_key_dialog(table) } certchoosebtn.signal_connect('clicked') { open_certificate_dialog(table) } append_page(table) set_page_title(table, "Import a public/private key pair") set_page_type(table, Assistant::PAGE_CONTENT) end def create_signature_info_page vbox = VBox.new(false, 5) lbl = Label.new("Here are a few optional information you can add with your signature.") vbox.pack_start(lbl, true, true, 0) labels = [ [ "Location:", @location = Entry.new ], [ "Contact:", @email = Entry.new ], [ "Reason:", @reason = Entry.new ] ] row = 0 table = Table.new(4, 3) labels.each do |label| table.attach(Label.new(label[0]).set_alignment(1,0), 0, 1, row, row + 1, Gtk::EXPAND | Gtk::FILL, Gtk::SHRINK, 4, 4) table.attach(label[1], 1, 2, row, row + 1, Gtk::EXPAND | Gtk::FILL, Gtk::SHRINK, 4, 4) row = row.succ end vbox.pack_start(table, true, true, 0) append_page(vbox) set_page_title(vbox, "Fill in signature details") set_page_type(vbox, Assistant::PAGE_CONFIRM) set_page_complete(vbox, true) end def create_termination_page @lastpage = VBox.new(false, 5) @msg_status = Label.new @lastpage.pack_start(@msg_status, true, true, 0) append_page(@lastpage) set_page_title(@lastpage, "Document has not been signed") set_page_type(@lastpage, Assistant::PAGE_SUMMARY) end end end end origami-2.0.0/bin/gui/treeview.rb0000644000004100000410000003453712757133666016737 0ustar www-datawww-data=begin This file is part of PDF Walker, a graphical PDF file browser Copyright (C) 2016 Guillaume Delugré. PDF Walker 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. PDF Walker 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 PDF Walker. If not, see . =end module PDFWalker class Walker < Window private def create_treeview @treeview = PDFTree.new(self).set_headers_visible(false) colcontent = Gtk::TreeViewColumn.new("Names", Gtk::CellRendererText.new.set_foreground_set(true).set_background_set(true), text: PDFTree::TEXTCOL, weight: PDFTree::WEIGHTCOL, style: PDFTree::STYLECOL, foreground: PDFTree::FGCOL, background: PDFTree::BGCOL ) @treeview.append_column(colcontent) end end class PDFTree < TreeView include Popable OBJCOL = 0 TEXTCOL = 1 WEIGHTCOL = 2 STYLECOL = 3 FGCOL = 4 BGCOL = 5 LOADCOL = 6 @@appearance = Hash.new(Weight: Pango::WEIGHT_NORMAL, Style: Pango::STYLE_NORMAL) attr_reader :parent def initialize(parent) @parent = parent reset_appearance @treestore = TreeStore.new(Object::Object, String, Pango::FontDescription::Weight, Pango::FontDescription::Style, String, String, Fixnum) super(@treestore) signal_connect('cursor-changed') { iter = selection.selected if iter obj = @treestore.get_value(iter, OBJCOL) parent.hexview.load(obj) parent.objectview.load(obj) end } signal_connect('row-activated') { |tree, path, column| if selection.selected obj = @treestore.get_value(selection.selected, OBJCOL) if row_expanded?(path) collapse_row(path) else expand_row(path, false) end goto(obj) if obj.is_a?(Origami::Reference) end } signal_connect('row-expanded') { |tree, iter, path| obj = @treestore.get_value(iter, OBJCOL) if obj.is_a?(Origami::Stream) and iter.n_children == 1 # Processing with an XRef or Object Stream if obj.is_a?(Origami::ObjectStream) obj.each { |embeddedobj| load_object(iter, embeddedobj) } elsif obj.is_a?(Origami::XRefStream) obj.each { |xref| load_xrefstm(iter, xref) } end end for i in 0...iter.n_children subiter = iter.nth_child(i) subobj = @treestore.get_value(subiter, OBJCOL) load_sub_objects(subiter, subobj) end } add_events(Gdk::Event::BUTTON_PRESS_MASK) signal_connect('button_press_event') { |widget, event| if event.button == 3 && parent.opened path = get_path(event.x,event.y).first set_cursor(path, nil, false) obj = @treestore.get_value(@treestore.get_iter(path), OBJCOL) popup_menu(obj, event, path) end } end def clear @treestore.clear end def goto(obj, follow_references: true) if obj.is_a?(TreePath) set_cursor(obj, nil, false) else if obj.is_a?(Origami::Name) and obj.parent.is_a?(Origami::Dictionary) and obj.parent.has_key?(obj) obj = obj.parent[obj] elsif obj.is_a?(Origami::Reference) and follow_references obj = begin obj.solve rescue Origami::InvalidReferenceError @parent.error("Object not found : #{obj}") return end end _, path = object_to_tree_pos(obj) if path.nil? @parent.error("Object not found : #{obj.type}") return end expand_to_path(path) unless row_expanded?(path) @parent.explorer_history << cursor.first if cursor.first set_cursor(path, nil, false) end end def highlight(obj, color) if obj.is_a?(Origami::Name) and obj.parent.is_a?(Origami::Dictionary) and obj.parent.has_key?(obj) obj = obj.parent[obj] end iter, path = object_to_tree_pos(obj) if iter.nil? or path.nil? @parent.error("Object not found : #{obj.type}") return end @treestore.set_value(iter, BGCOL, color) expand_to_path(path) unless row_expanded?(path) end def load(pdf) return unless pdf self.clear begin # # Create root entry # root = @treestore.append(nil) @treestore.set_value(root, OBJCOL, pdf) set_node(root, :Filename, @parent.filename) # # Create header entry # header = @treestore.append(root) @treestore.set_value(header, OBJCOL, pdf.header) set_node(header, :Header, "Header (version #{pdf.header.major_version}.#{pdf.header.minor_version})") no = 1 pdf.revisions.each { |revision| load_revision(root, no, revision) no = no + 1 } set_model(@treestore) ensure expand(@treestore.iter_first, 3) set_cursor(@treestore.iter_first.path, nil, false) end end private def object_to_tree_pos(obj) # Locate the indirect object. root_obj = obj object_path = [ root_obj ] while root_obj.parent root_obj = root_obj.parent object_path.push(root_obj) end @treestore.each do |model, path, iter| current_obj = @treestore.get_value(iter, OBJCOL) # Load the intermediate nodes if necessary. if object_path.any?{|object| object.equal?(current_obj)} load_sub_objects(iter, current_obj) end # Unfold the object stream if it's in the object path. if obj.is_a?(Origami::Object) and current_obj.is_a?(Origami::ObjectStream) and root_obj.equal?(current_obj) and iter.n_children == 1 current_obj.each { |embeddedobj| load_object(iter, embeddedobj) } end return [ iter, path ] if obj.equal?(current_obj) end nil end def expand(row, depth) if row and depth != 0 loop do expand_row(row.path, false) expand(row.first_child, depth - 1) break if not row.next! end end end def load_revision(root, no, revision) revroot = @treestore.append(root) @treestore.set_value(revroot, OBJCOL, revision) set_node(revroot, :Revision, "Revision #{no}") load_body(revroot, revision.body.values) load_xrefs(revroot, revision.xreftable) load_trailer(revroot, revision.trailer) end def load_body(rev, body) bodyroot = @treestore.append(rev) @treestore.set_value(bodyroot, OBJCOL, body) set_node(bodyroot, :Body, "Body") body.sort_by{|obj| obj.file_offset.to_i }.each { |object| begin load_object(bodyroot, object) rescue msg = "#{$!.class}: #{$!.message}\n#{$!.backtrace.join($/)}" STDERR.puts(msg) #@parent.error(msg) next end } end def load_object(container, object, depth = 1, name = nil) iter = @treestore.append(container) @treestore.set_value(iter, OBJCOL, object) type = object.native_type.to_s.split('::').last.to_sym if name.nil? name = case object when Origami::String '"' + object.to_utf8.gsub("\x00", ".") + '"' when Origami::Number, Origami::Name object.value.to_s else object.type.to_s end end set_node(iter, type, name) return unless depth > 0 load_sub_objects(iter, object, depth) end def load_sub_objects(container, object, depth = 1) return unless depth > 0 and @treestore.get_value(container, LOADCOL) != 1 case object when Origami::Array object.each do |subobject| load_object(container, subobject, depth - 1) end when Origami::Dictionary object.each_key do |subkey| load_object(container, object[subkey.value], depth - 1, subkey.value.to_s) end when Origami::Stream load_object(container, object.dictionary, depth - 1, "Stream Dictionary") end @treestore.set_value(container, LOADCOL, 1) end def load_xrefstm(stm, embxref) xref = @treestore.append(stm) @treestore.set_value(xref, OBJCOL, embxref) if embxref.is_a?(Origami::XRef) set_node(xref, :XRef, embxref.to_s.chomp) else set_node(xref, :XRef, "xref to ObjectStream #{embxref.objstmno}, object index #{embxref.index}") end end def load_xrefs(rev, table) return unless table section = @treestore.append(rev) @treestore.set_value(section, OBJCOL, table) set_node(section, :XRefSection, "XRef section") table.each_subsection { |subtable| subsection = @treestore.append(section) @treestore.set_value(subsection, OBJCOL, subtable) set_node(subsection, :XRefSubSection, "#{subtable.range.begin} #{subtable.range.end - subtable.range.begin + 1}") subtable.each { |entry| xref = @treestore.append(subsection) @treestore.set_value(xref, OBJCOL, entry) set_node(xref, :XRef, entry.to_s.chomp) } } end def load_trailer(rev, trailer) trailer_root = @treestore.append(rev) @treestore.set_value(trailer_root, OBJCOL, trailer) set_node(trailer_root, :Trailer, "Trailer") load_object(trailer_root, trailer.dictionary) unless trailer.dictionary.nil? end def reset_appearance @@appearance[:Filename] = {Weight: Pango::WEIGHT_BOLD, Style: Pango::STYLE_NORMAL} @@appearance[:Header] = {Color: "darkgreen", Weight: Pango::WEIGHT_BOLD, Style: Pango::STYLE_NORMAL} @@appearance[:Revision] = {Color: "blue", Weight: Pango::WEIGHT_BOLD, Style: Pango::STYLE_NORMAL} @@appearance[:Body] = {Color: "purple", Weight: Pango::WEIGHT_BOLD, Style: Pango::STYLE_NORMAL} @@appearance[:XRefSection] = {Color: "purple", Weight: Pango::WEIGHT_BOLD, Style: Pango::STYLE_NORMAL} @@appearance[:XRefSubSection] = {Color: "brown", Weight: Pango::WEIGHT_BOLD, Style: Pango::STYLE_NORMAL} @@appearance[:XRef] = {Color: "gray20", Weight: Pango::WEIGHT_BOLD, Style: Pango::STYLE_NORMAL} @@appearance[:Trailer] = {Color: "purple", Weight: Pango::WEIGHT_BOLD, Style: Pango::STYLE_NORMAL} @@appearance[:StartXref] = {Weight: Pango::WEIGHT_BOLD, Style: Pango::STYLE_NORMAL} @@appearance[:String] = {Color: "red", Weight: Pango::WEIGHT_NORMAL, Style: Pango::STYLE_ITALIC} @@appearance[:Name] = {Color: "gray", Weight: Pango::WEIGHT_NORMAL, Style: Pango::STYLE_ITALIC} @@appearance[:Number] = {Color: "orange", Weight: Pango::WEIGHT_NORMAL, Style: Pango::STYLE_NORMAL} @@appearance[:Dictionary] = {Color: "brown", Weight: Pango::WEIGHT_BOLD, Style: Pango::STYLE_NORMAL} @@appearance[:Stream] = {Color: "darkcyan", Weight: Pango::WEIGHT_BOLD, Style: Pango::STYLE_NORMAL} @@appearance[:StreamData] = {Color: "darkcyan", Weight: Pango::WEIGHT_NORMAL, Style: Pango::STYLE_OBLIQUE} @@appearance[:Array] = {Color: "darkgreen", Weight: Pango::WEIGHT_BOLD, Style: Pango::STYLE_NORMAL} @@appearance[:Reference] = {Weight: Pango::WEIGHT_NORMAL, Style: Pango::STYLE_OBLIQUE} @@appearance[:Boolean] = {Color: "deeppink", Weight: Pango::WEIGHT_NORMAL, Style: Pango::STYLE_NORMAL} end def get_object_appearance(type) @@appearance[type] end def set_node(node, type, text) @treestore.set_value(node, TEXTCOL, text) app = get_object_appearance(type) @treestore.set_value(node, WEIGHTCOL, app[:Weight]) @treestore.set_value(node, STYLECOL, app[:Style]) @treestore.set_value(node, FGCOL, app[:Color]) end end end origami-2.0.0/bin/gui/COPYING0000644000004100000410000010451312757133666015603 0ustar www-datawww-data GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program 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. This program 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 this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . origami-2.0.0/bin/pdfmetadata0000755000004100000410000000602212757133666016160 0ustar www-datawww-data#!/usr/bin/env ruby =begin = Info Prints out the metadata contained in a PDF document. = License Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end begin require 'origami' rescue LoadError $: << File.join(__dir__, '../lib') require 'origami' end include Origami require 'colorize' require 'optparse' class OptParser BANNER = <] [-i] [-x] Prints out the metadata contained in a PDF document. Bug reports or feature requests at: http://github.com/gdelugre/origami Options: USAGE def self.parser(options) OptionParser.new do |opts| opts.banner = BANNER opts.on("-i", "Extracts document info metadata") do |i| options[:doc_info] = true end opts.on("-x", "Extracts XMP document metadata stream") do |i| options[:doc_stream] = true end opts.on("-n", "Turn off colorized output.") do options[:disable_colors] = true end opts.on_tail("-h", "--help", "Show this message") do puts opts exit end end end def self.parse(args) options = { disable_colors: false } self.parser(options).parse!(args) options end end begin @options = OptParser.parse(ARGV) unless @options[:doc_info] or @options[:doc_stream] @options[:doc_info] = @options[:doc_stream] = true end String.disable_colorization @options[:disable_colors] target = (ARGV.empty?) ? STDIN : ARGV.shift params = { verbosity: Parser::VERBOSE_QUIET, lazy: true } pdf = PDF.read(target, params) if @options[:doc_info] and pdf.document_info? puts "[*] Document information dictionary:".magenta docinfo = pdf.document_info docinfo.each_pair do |name, item| obj = item.solve print name.value.to_s.ljust(20, ' ').green puts ": #{obj.respond_to?(:to_utf8) ? obj.to_utf8 : obj.value}" end puts end if @options[:doc_stream] and pdf.metadata? puts "[*] Metadata stream:".magenta metadata = pdf.metadata metadata.each_pair do |name, item| print name.ljust(20, ' ').green puts ": #{item}" end end rescue abort "#{$!.class}: #{$!.message}" end origami-2.0.0/bin/pdfcop0000755000004100000410000003331412757133666015165 0ustar www-datawww-data#!/usr/bin/env ruby =begin = Info This is a PDF document filtering engine using Origami. Security policies are based on a white list of PDF features. Default policies details can be found in the default configuration file. You can also add your own policy and activate it using the -p switch. = License Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end begin require 'origami' rescue LoadError $: << File.join(__dir__, '../lib') require 'origami' end require 'optparse' require 'yaml' require 'rexml/document' require 'digest/md5' require 'colorize' DEFAULT_CONFIG_FILE = "#{File.dirname(__FILE__)}/config/pdfcop.conf.yml" DEFAULT_POLICY = "standard" SECURITY_POLICIES = {} def load_config_file(path) SECURITY_POLICIES.update(Hash.new(false).update YAML.load(File.read(path))) end class OptParser BANNER = < The PDF filtering engine. Scans PDF documents for malicious structures. Bug reports or feature requests at: http://github.com/gdelugre/origami Options: USAGE def self.parse(args) options = {colors: true} opts = OptionParser.new do |opts| opts.banner = BANNER opts.on("-o", "--output LOG_FILE", "Output log file (default STDOUT)") do |o| options[:output_log] = o end opts.on("-c", "--config CONFIG_FILE", "Load security policies from given configuration file") do |cf| options[:config_file] = cf end opts.on("-p", "--policy POLICY_NAME", "Specify applied policy. Predefined policies: 'none', 'standard', 'strong', 'paranoid'") do |p| options[:policy] = p end opts.on("-n", "--no-color", "Turn off colorized output") do options[:disable_colors] = true end opts.on_tail("-h", "--help", "Show this message") do puts opts exit end end opts.parse!(args) options end end @options = OptParser.parse(ARGV) if @options.has_key?(:output_log) LOGGER = File.open(@options[:output_log], "a+") else LOGGER = STDOUT end if not @options.has_key?(:policy) @options[:policy] = DEFAULT_POLICY end String.disable_colorization @options[:disable_colors] load_config_file(@options[:config_file] || DEFAULT_CONFIG_FILE) unless SECURITY_POLICIES.has_key?("POLICY_#{@options[:policy].upcase}") abort "Undeclared policy `#{@options[:policy]}'" end if ARGV.empty? abort "Error: No filename was specified. #{$0} --help for details." else TARGET = ARGV.shift end def log(str, color = :default) LOGGER.puts("[#{Time.now}]".cyan + " #{str.colorize(color)}") end def reject(cause) log("Document rejected by policy `#{@options[:policy]}', caused by #{cause.inspect}.", :red) abort end def check_rights(*required_rights) current_rights = SECURITY_POLICIES["POLICY_#{@options[:policy].upcase}"] reject(required_rights) if required_rights.any?{|right| current_rights[right.to_s] == false} end def analyze_xfa_forms(xfa) case xfa when Origami::Array then xml = "" i = 0 xfa.each do |packet| if i % 2 == 1 xml << packet.solve.data end i = i + 1 end when Origami::Stream then xml = xfa.data else reject("Malformed XFA dictionary") end xfadoc = REXML::Document.new(xml) REXML::XPath.match(xfadoc, "//script").each do |script| case script.attributes["contentType"] when "application/x-formcalc" then check_rights(:allowFormCalc) else check_rights(:allowJS) end end end def analyze_annotation(annot, level = 0) check_rights(:allowAnnotations) if annot.is_a?(Origami::Dictionary) and annot.has_key?(:Subtype) case annot[:Subtype].solve.value when :FileAttachment check_rights(:allowAttachments, :allowFileAttachmentAnnotation) when :Sound check_rights(:allowSoundAnnotation) when :Movie check_rights(:allowMovieAnnotation) when :Screen check_rights(:allowScreenAnnotation) when :Widget check_rights(:allowAcroforms) when :"3D" check_rights(:allow3DAnnotation) # 3D annotation might pull in JavaScript for real-time driven behavior. if annot.has_key?(:"3DD") dd = annot[:"3DD"].solve u3dstream = nil case dd when Origami::Stream u3dstream = dd when Origami::Dictionary u3dstream = dd[:"3DD"] end if u3dstream and u3dstream.has_field?(:OnInstantiate) check_rights(:allowJS) if annot.has_key?(:"3DA") # is 3d view instantiated automatically? u3dactiv = annot[:"3DA"].solve check_rights(:allowJSAtOpening) if u3dactiv.is_a?(Origami::Dictionary) and (u3dactiv[:A] == :PO or u3dactiv[:A] == :PV) end end end when :RichMedia check_rights(:allowRichMediaAnnotation) end end end def analyze_page(page, level = 0) section_prefix = " " * 2 * level + ">" * (level + 1) log(section_prefix + " Inspecting page...") text_prefix = " " * 2 * (level + 1) + "." * (level + 1) if page.is_a?(Origami::Dictionary) # # Checking page additional actions. # if page.has_key?(:AA) if page.AA.is_a?(Origami::Dictionary) log(text_prefix + " Page has an action dictionary.") aa = Origami::Page::AdditionalActions.new(page.AA); aa.parent = page.AA.parent analyze_action(aa.O, true, level + 1) if aa.has_key?(:O) analyze_action(aa.C, false, level + 1) if aa.has_key?(:C) end end # # Looking for page annotations. # page.each_annotation do |annot| analyze_annotation(annot, level + 1) end end end def analyze_action(action, triggered_at_opening, level = 0) section_prefix = " " * 2 * level + ">" * (level + 1) log(section_prefix + " Inspecting action...") text_prefix = " " * 2 * (level + 1) + "." * (level + 1) if action.is_a?(Origami::Dictionary) log(text_prefix + " Found #{action[:S]} action.") type = action[:S].is_a?(Origami::Reference) ? action[:S].solve : action[:S] case type.value when :JavaScript check_rights(:allowJS) check_rights(:allowJSAtOpening) if triggered_at_opening when :Launch check_rights(:allowLaunchAction) when :Named check_rights(:allowNamedAction) when :GoTo check_rights(:allowGoToAction) dest = action[:D].is_a?(Origami::Reference) ? action[:D].solve : action[:D] if dest.is_a?(Origami::Array) and dest.length > 0 and dest.first.is_a?(Origami::Reference) dest_page = dest.first.solve if dest_page.is_a?(Origami::Page) log(text_prefix + " Destination page found.") analyze_page(dest_page, level + 1) end end when :GoToE check_rights(:allowAttachments,:allowGoToEAction) when :GoToR check_rights(:allowGoToRAction) when :Thread check_rights(:allowGoToRAction) if action.has_key?(:F) when :URI check_rights(:allowURIAction) when :SubmitForm check_rights(:allowAcroForms,:allowSubmitFormAction) when :ImportData check_rights(:allowAcroForms,:allowImportDataAction) when :Rendition check_rights(:allowScreenAnnotation,:allowRenditionAction) when :Sound check_rights(:allowSoundAnnotation,:allowSoundAction) when :Movie check_rights(:allowMovieAnnotation,:allowMovieAction) when :RichMediaExecute check_rights(:allowRichMediaAnnotation,:allowRichMediaAction) when :GoTo3DView check_rights(:allow3DAnnotation,:allowGoTo3DAction) end if action.has_key?(:Next) log(text_prefix + "This action is chained to another action!") check_rights(:allowChainedActions) analyze_action(action.Next) end elsif action.is_a?(Origami::Array) dest = action if dest.length > 0 and dest.first.is_a?(Origami::Reference) dest_page = dest.first.solve if dest_page.is_a?(Origami::Page) log(text_prefix + " Destination page found.") check_rights(:allowGoToAction) analyze_page(dest_page, level + 1) end end end end begin log("PDFcop is running on target `#{TARGET}', policy = `#{@options[:policy]}'", :green) log(" File size: #{File.size(TARGET)} bytes", :magenta) log(" MD5: #{Digest::MD5.hexdigest(File.read(TARGET))}", :magenta) @pdf = Origami::PDF.read(TARGET, verbosity: Origami::Parser::VERBOSE_QUIET, ignore_errors: SECURITY_POLICIES["POLICY_#{@options[:policy].upcase}"]['allowParserErrors'] ) log("> Inspecting document structure...", :yellow) if @pdf.encrypted? log(" . Encryption = YES") check_rights(:allowEncryption) end log("> Inspecting document catalog...", :yellow) catalog = @pdf.Catalog reject("Invalid document catalog") unless catalog.is_a?(Origami::Catalog) if catalog.has_key?(:OpenAction) log(" . OpenAction entry = YES") check_rights(:allowOpenAction) action = catalog.OpenAction analyze_action(action, true, 1) end if catalog.has_key?(:AA) if catalog.AA.is_a?(Origami::Dictionary) aa = Origami::CatalogAdditionalActions.new(catalog.AA); aa.parent = catalog; log(" . Additional actions dictionary = YES") analyze_action(aa.WC, false, 1) if aa.has_key?(:WC) analyze_action(aa.WS, false, 1) if aa.has_key?(:WS) analyze_action(aa.DS, false, 1) if aa.has_key?(:DS) analyze_action(aa.WP, false, 1) if aa.has_key?(:WP) analyze_action(aa.DP, false, 1) if aa.has_key?(:DP) end end if catalog.has_key?(:AcroForm) acroform = catalog.AcroForm if acroform.is_a?(Origami::Dictionary) log(" . AcroForm = YES") check_rights(:allowAcroForms) if acroform.has_key?(:XFA) log(" . XFA = YES") check_rights(:allowXFAForms) analyze_xfa_forms(acroform[:XFA].solve) end end end log("> Inspecting JavaScript names directory...", :yellow) if @pdf.each_named_script.any? check_rights(:allowJS) check_rights(:allowJSAtOpening) end log("> Inspecting attachment names directory...", :yellow) if @pdf.each_attachment.any? check_rights(:allowAttachments) end log("> Inspecting document pages...", :yellow) @pdf.each_page do |page| analyze_page(page, 1) end log("> Inspecting document streams...", :yellow) @pdf.indirect_objects.find_all{|obj| obj.is_a?(Origami::Stream)}.each do |stream| if stream.dictionary.has_key?(:Filter) filters = stream.Filter filters = [ filters ] if filters.is_a?(Origami::Name) if filters.is_a?(Origami::Array) filters.each do |filter| case filter.value when :ASCIIHexDecode check_rights(:allowASCIIHexFilter) when :ASCII85Decode check_rights(:allowASCII85Filter) when :LZWDecode check_rights(:allowLZWFilter) when :FlateDecode check_rights(:allowFlateDecode) when :RunLengthDecode check_rights(:allowRunLengthFilter) when :CCITTFaxDecode check_rights(:allowCCITTFaxFilter) when :JBIG2Decode check_rights(:allowJBIG2Filter) when :DCTDecode check_rights(:allowDCTFilter) when :JPXDecode check_rights(:allowJPXFilter) when :Crypt check_rights(:allowCryptFilter) end end end end end # # TODO: Detect JS at opening in XFA (check event tag) # Check image encoding in XFA ? # Only allow valid signed documents ? # Recursively scan attached files. # On-the-fly injection of prerun JS code to hook vulnerable methods (dynamic exploit detection) ??? # ... # log("Document accepted by policy `#{@options[:policy]}'.", :green) rescue log("An error occured during analysis : #{$!.class} (#{$!.message})") reject("Analysis failure") ensure LOGGER.close end origami-2.0.0/bin/pdfencrypt0000755000004100000410000000573112757133666016072 0ustar www-datawww-data#!/usr/bin/env ruby =begin = Info Encrypts a PDF document. = License Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end begin require 'origami' rescue LoadError $: << File.join(__dir__, '../lib') require 'origami' end include Origami require 'optparse' class OptParser BANNER = <] [-p ] [-c ] [-s ] [--hardened] [-o ] Encrypts a PDF document. Supports RC4 40 to 128 bits, AES128, AES256. Bug reports or feature requests at: http://github.com/gdelugre/origami Options: USAGE def self.parser(options) OptionParser.new do |opts| opts.banner = BANNER opts.on("-o", "--output FILE", "Output PDF file (stdout by default)") do |o| options[:output] = o end opts.on("-p", "--password PASSWORD", "Password of the document") do |p| options[:password] = p end opts.on("-c", "--cipher CIPHER", "Cipher used to encrypt the document (Default: AES)") do |c| options[:cipher] = c end opts.on("-s", "--key-size KEYSIZE", "Key size in bits (Default: 128)") do |s| options[:key_size] = s.to_i end opts.on("--hardened", "Use stronger key validation scheme (only AES-256)") do options[:hardened] = true end opts.on_tail("-h", "--help", "Show this message") do puts opts exit end end end def self.parse(args) options = { output: STDOUT, password: '', cipher: 'aes', key_size: 128, hardened: false } self.parser(options).parse!(args) options end end begin @options = OptParser.parse(ARGV) target = (ARGV.empty?) ? STDIN : ARGV.shift params = { verbosity: Parser::VERBOSE_QUIET, } pdf = PDF.read(target, params) pdf.encrypt( user_passwd: @options[:password], owner_passwd: @options[:password], cipher: @options[:cipher], key_size: @options[:key_size], hardened: @options[:hardened] ) pdf.save(@options[:output], noindent: true) rescue abort "#{$!.class}: #{$!.message}" end origami-2.0.0/bin/pdfexplode0000755000004100000410000001677012757133666016053 0ustar www-datawww-data#!/usr/bin/env ruby =begin = Info Explodes a PDF into separate documents. = License Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end begin require 'origami' rescue LoadError $: << File.join(__dir__, '../lib') require 'origami' end include Origami require 'optparse' require 'rexml/document' class OptParser BANNER = < [-r ] [-t pages|rsrc] [-d ] Explodes a document into separate documents. Bug reports or feature requests at: http://github.com/gdelugre/origami Options: USAGE def self.parser(options) OptionParser.new do |opts| opts.banner = BANNER opts.on("-d", "--output-dir DIR", "Output directory.") do |d| options[:output_dir] = d end opts.on("-r", "--range PAGES", "Page range (e.g: 2-, 1-3, 5). Default to '-'.") do |r| range = if r.index('-').nil? page = r.to_i Range.new(page-1, page-1) else from, to = r.split('-').map{|bound| bound.to_i} from ||= 1 to ||= 0 Range.new(from-1, to-1) end options[:page_range] = range end opts.on("-t", "--type TYPE", "Split by type. Can be 'pages' or 'rsrc'. Default to 'pages'.") do |t| options[:split_by] = t end opts.on_tail("-h", "--help", "Show this message.") do puts opts exit end end end def self.parse(args) options = { page_range: (0..-1), split_by: 'pages' } self.parser(options).parse!(args) options end end begin @options = OptParser.parse(ARGV) if ARGV.empty? abort "Error: No filename was specified. #{$0} --help for details." else target = ARGV.shift end if @options[:output_dir].nil? @options[:output_dir] = "#{File.join(File.dirname(target), File.basename(target,'.pdf'))}.explode" end Origami::OPTIONS[:ignore_bad_references] = true OUTPUT_DIR = @options[:output_dir] Dir::mkdir(OUTPUT_DIR) unless File.directory?(OUTPUT_DIR) def split_by_rsrc(n, page, type) all_rsrc = page.resources type_rsrc = page.resources(type) other_rsrc = all_rsrc.keys - type_rsrc.keys unless type_rsrc.empty? # Keep only specified resource type. output_file = File.join(OUTPUT_DIR, "page_#{n}_keeponly_#{type}.pdf") PDF.write(output_file) do |pdf| reduced = page.copy # New resource dictionary with only matching resources. reduced.Resources = Resources.new(type => type_rsrc) # Remove mention of other resources. reduced.each_content_stream do |stream| stream.data = stream.data.lines. delete_if {|line| other_rsrc.any?{|rsrc| line =~ /#{rsrc}/}}.join end STDERR.puts "Creating #{output_file}..." pdf.append_page(reduced) end # Remove all specified resource type. output_file = File.join(OUTPUT_DIR, "page_#{n}_excluded_#{type}.pdf") PDF.write(output_file) do |pdf| reduced = page.copy # New resource dictionary with no resource of specified type. reduced.Resources = reduced.Resources.copy reduced.Resources.delete(type) # Remove mention this resource type. reduced.each_content_stream do |stream| stream.data = stream.data.lines. delete_if {|line| type_rsrc.keys.any?{|rsrc| line =~ /#{rsrc}/}}.join end STDERR.puts "Creating #{output_file}..." pdf.append_page(reduced) end # Now treating each resource object separately. type_rsrc.each_pair do |name, rsrc| anyother_rsrc = all_rsrc.keys - [ name ] # Keey only specified resource object. output_file = File.join(OUTPUT_DIR, "page_#{n}_keeponly_#{type}_#{name}.pdf") PDF.write(output_file) do |pdf| reduced = page.copy # New resource dictionary with only specified resource object. reduced.Resources = Resources.new(type => {name => rsrc}) # Remove mention of all other resources. reduced.each_content_stream do |stream| stream.data = stream.data.lines. delete_if {|line| anyother_rsrc.any?{|rsrc| line =~ /#{rsrc}/}}.join end STDERR.puts "Creating #{output_file}..." pdf.append_page(reduced) end # Remove only specified resource object. output_file = File.join(OUTPUT_DIR, "page_#{n}_excluded_#{type}_#{name}.pdf") PDF.write(output_file) do |pdf| reduced = page.copy # New resource dictionary with only specified resource object. reduced.Resources = reduced.Resources.copy reduced.Resources[type] = reduced.Resources.send(type).copy reduced.Resources[type].delete(name) # Remove mention of this resource only. reduced.each_content_stream do |stream| stream.data = stream.data.lines. delete_if {|line| line =~ /#{name}/}.join end STDERR.puts "Creating #{output_file}..." pdf.append_page(reduced) end end end end params = { verbosity: Parser::VERBOSE_QUIET, } pdf = PDF.read(target, params) i = @options[:page_range].first + 1 pdf.pages[@options[:page_range]].each do |page| case @options[:split_by] when 'pages' output_file = File.join(OUTPUT_DIR, "page_#{i}.pdf") PDF.write(output_file) do |pdf| STDERR.puts "Creating #{output_file}..." pdf.append_page(page) end when 'rsrc' [ Resources::EXTGSTATE, Resources::COLORSPACE, Resources::PATTERN, Resources::SHADING, Resources::XOBJECT, Resources::FONT, Resources::PROPERTIES ].each { |type| split_by_rsrc(i, page, type) } else raise ArgumentError, "Unknown split option: #{@options[:split_by]}" end i += 1 end rescue abort "#{$!.class}: #{$!.message} #{$!.backtrace.join($/)}" end origami-2.0.0/bin/pdfdecompress0000755000004100000410000000451712757133666016553 0ustar www-datawww-data#!/usr/bin/env ruby =begin = Info Uncompresses all binary streams of a PDF document. = License Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end begin require 'origami' rescue LoadError $: << File.join(__dir__, '../lib') require 'origami' end include Origami require 'optparse' class OptParser BANNER = <] [-p ] [-o ] Uncompresses all binary streams of a PDF document. Bug reports or feature requests at: http://github.com/gdelugre/origami Options: USAGE def self.parser(options) OptionParser.new do |opts| opts.banner = BANNER opts.on("-o", "--output FILE", "Output PDF file (stdout by default)") do |o| options[:output] = o end opts.on_tail("-h", "--help", "Show this message") do puts opts exit end end end def self.parse(args) options = { output: STDOUT, } self.parser(options).parse!(args) options end end begin @options = OptParser.parse(ARGV) target = (ARGV.empty?) ? STDIN : ARGV.shift params = { verbosity: Parser::VERBOSE_QUIET, } pdf = PDF.read(target, params) pdf.root_objects.find_all { |obj| obj.is_a?(Stream) }.each { |stream| unless stream.filters.any?{|filter| %i[JPXDecode DCTDecode JBIG2Decode].include?(filter.value) } stream.encoded_data = stream.data stream.dictionary.delete(:Filter) end } pdf.save(@options[:output], noindent: true) rescue STDERR.puts $!.backtrace.join($/) abort "#{$!.class}: #{$!.message}" end origami-2.0.0/bin/pdfsh0000755000004100000410000000035612757133666015016 0ustar www-datawww-data#!/usr/bin/env ruby begin require 'irb' rescue LoadError abort "Error: you need to install irb to run this application." end $:.unshift File.join(__dir__, 'shell') ENV["IRBRC"] = File.join(__dir__, "shell", ".irbrc") IRB.start origami-2.0.0/bin/config/0000755000004100000410000000000012757133666015225 5ustar www-datawww-dataorigami-2.0.0/bin/config/pdfcop.conf.yml0000644000004100000410000001230312757133666020146 0ustar www-datawww-data--- POLICY_NONE: # # General features. # allowParserErrors: true allowAttachments: true allowEncryption: true allowFormCalc: true allowJSAtOpening: true allowJS: true allowAcroForms: true allowXFAForms: true # # Page annotations. # allowAnnotations: true allow3DAnnotation: true allowFileAttachmentAnnotation: true allowMovieAnnotation: true allowRichMediaAnnotation: true allowScreenAnnotation: true allowSoundAnnotation: true # # PDF Actions. # allowChainedActions: true allowOpenAction: true allowGoTo3DAction: true allowGoToAction: true allowGoToEAction: true allowGoToRAction: true allowImportDataAction: true allowJSAction: true allowLaunchAction: true allowMovieAction: true allowNamedAction: true allowRenditionAction: true allowRichMediaAction: true allowSoundAction: true allowSubmitFormAction: true allowURIAction: true # # Stream filters. # allowASCII85Filter: true allowASCIIHexFilter: true allowCCITTFaxFilter: true allowCryptFilter: true allowDCTFilter: true allowFlateFilter: true allowJBIG2Filter: true allowJPXFilter: true allowLZWFilter: true allowRunLengthFilter: true POLICY_STANDARD: # # General features. # allowParserErrors: false allowAttachments: false allowAcroForms: true allowEncryption: true allowFormCalc: true allowJS: true allowJSAtOpening: false allowXFAForms: true # # Page annotations. # allowAnnotations: true allow3DAnnotation: false allowFileAttachmentAnnotation: false allowMovieAnnotation: false allowRichMediaAnnotation: false allowScreenAnnotation: false allowSoundAnnotation: false # # PDF Actions. # allowChainedActions: true allowOpenAction: true allowGoTo3DAction: false allowGoToAction: true allowGoToEAction: false allowGoToRAction: false allowImportDataAction: false allowJSAction: true allowLaunchAction: false allowMovieAction: false allowNamedAction: false allowRenditionAction: false allowRichMediaAction: false allowSoundAction: false allowSubmitFormAction: true allowURIAction: true # # Stream filters. # allowASCII85Filter: false allowASCIIHexFilter: false allowCCITTFaxFilter: true allowCryptFilter: true allowDCTFilter: true allowFlateFilter: true allowJBIG2Filter: false allowJPXFilter: false allowLZWFilter: false allowRunLengthFilter: false POLICY_STRONG: # # General features. # allowParserErrors: false allowAttachments: false allowAcroForms: false allowEncryption: true allowFormCalc: true allowJS: false allowJSAtOpening: false allowXFAForms: false # # Page annotations. # allowAnnotations: true allow3DAnnotation: false allowFileAttachmentAnnotation: false allowMovieAnnotation: false allowRichMediaAnnotation: false allowScreenAnnotation: false allowSoundAnnotation: false # # PDF Actions. # allowChainedActions: false allowOpenAction: true allowGoTo3DAction: false allowGoToAction: true allowGoToEAction: false allowGoToRAction: false allowImportDataAction: false allowJSAction: false allowLaunchAction: false allowMovieAction: false allowNamedAction: false allowRenditionAction: false allowRichMediaAction: false allowSoundAction: false allowSubmitFormAction: false allowURIAction: true # # Stream filters. # allowASCII85Filter: false allowASCIIHexFilter: false allowCCITTFaxFilter: false allowCryptFilter: true allowDCTFilter: true allowFlateFilter: true allowJBIG2Filter: false allowJPXFilter: false allowLZWFilter: false allowRunLengthFilter: false POLICY_PARANOID: # # General features. # allowParserErrors: false allowAttachments: false allowAcroForms: false allowEncryption: false allowFormCalc: false allowJS: false allowJSAtOpening: false allowXFAForms: false # # Page annotations. # allowAnnotations: true allow3DAnnotation: false allowFileAttachmentAnnotation: false allowMovieAnnotation: false allowRichMediaAnnotation: false allowScreenAnnotation: false allowSoundAnnotation: false # # PDF Actions. # allowChainedActions: false allowOpenAction: false allowGoTo3DAction: false allowGoToAction: true allowGoToEAction: false allowGoToRAction: false allowImportDataAction: false allowJSAction: false allowLaunchAction: false allowMovieAction: false allowNamedAction: false allowRenditionAction: false allowRichMediaAction: false allowSoundAction: false allowSubmitFormAction: false allowURIAction: false # # Stream filters. # allowASCII85Filter: false allowASCIIHexFilter: false allowCCITTFaxFilter: false allowCryptFilter: false allowDCTFilter: true allowFlateFilter: true allowJBIG2Filter: false allowJPXFilter: false allowLZWFilter: false allowRunLengthFilter: false origami-2.0.0/bin/shell/0000755000004100000410000000000012757133666015067 5ustar www-datawww-dataorigami-2.0.0/bin/shell/hexdump.rb0000644000004100000410000000341312757133666017067 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami 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. Origami 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 Origami. If not, see . =end require 'colorize' class String #:nodoc: def hexdump(bytesperline: 16, upcase: true, offsets: true, delta: 0) dump = "" counter = 0 while counter < length offset = sprintf("%010X", counter + delta) linelen = (counter < length - bytesperline) ? bytesperline : (length - counter) bytes = "" linelen.times do |i| byte = self[counter + i].ord.to_s(16) byte.insert(0, "0") if byte.size < 2 bytes << byte bytes << " " unless i == bytesperline - 1 end ascii = self[counter, linelen].ascii_print if upcase offset.upcase! bytes.upcase! end dump << "#{offset.yellow if offsets} #{bytes.to_s.ljust(bytesperline * 3 - 1).bold} #{ascii}\n" counter += bytesperline end dump end def ascii_print self.gsub(/[^[[:print:]]]/, ".") end end origami-2.0.0/bin/shell/.irbrc0000644000004100000410000000326412757133666016176 0ustar www-datawww-databegin require 'origami' rescue LoadError $: << File.join(__dir__, '../../lib') require 'origami' end include Origami require 'console.rb' require 'readline' OPENSSL_SUPPORT = (defined?(OpenSSL).nil?) ? 'no' : 'yes' JAVASCRIPT_SUPPORT = (defined?(PDF::JavaScript::Engine).nil?) ? 'no' : 'yes' DEFAULT_BANNER = "Welcome to the PDF shell (Origami release #{Origami::VERSION}) [OpenSSL: #{OPENSSL_SUPPORT}, JavaScript: #{JAVASCRIPT_SUPPORT}]\n" def set_completion completionProc = proc { |input| bind = IRB.conf[:MAIN_CONTEXT].workspace.binding case input when /^(.*)::$/ begin space = eval("Origami::#{$1}", bind) rescue Exception return [] end return space.constants.reject{|const| space.const_get(const) <= Exception} when /^(.*).$/ begin space = eval($1, bind) rescue return [] end return space.public_methods end } if Readline.respond_to?("basic_word_break_characters=") Readline.basic_word_break_characters= " \t\n\"\\'`><=;|&{(" end Readline.completion_append_character = nil Readline.completion_proc = completionProc end def set_prompt IRB.conf[:PROMPT][:PDFSH] = { PROMPT_C: "?>> ", RETURN: "%s\n", PROMPT_I: ">>> ", PROMPT_N: ">>> ", PROMPT_S: nil } IRB.conf[:PROMPT_MODE] = :PDFSH IRB.conf[:AUTO_INDENT] = true end # Print the shell banner. puts DEFAULT_BANNER.green # Import the type conversion helper routines. TOPLEVEL_BINDING.eval("using Origami::TypeConversion") #set_completion set_prompt origami-2.0.0/bin/shell/console.rb0000644000004100000410000000674112757133666017066 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end require 'tempfile' require 'hexdump' require 'colorize' String.disable_colorization(false) module Origami module Object def inspect to_s end end class Stream def edit(editor = ENV['EDITOR']) Tempfile.open("origami") do |tmpfile| tmpfile.write(self.data) tmpfile.flush Process.wait Kernel.spawn "#{editor} #{tmpfile.path}" self.data = File.read(tmpfile.path) tmpfile.unlink end true end def inspect self.data.hexdump end end class Page < Dictionary def edit each_content_stream do |stream| stream.edit end end end class PDF if defined?(PDF::JavaScript::Engine) class JavaScript::Engine def shell while (print 'js> '.magenta; line = gets) begin puts exec(line) rescue V8::JSError => e puts "Error: #{e.message}" end end end end end class Revision def to_s puts "---------- Body ----------".white.bold @body.each_value { |obj| print "#{obj.reference.to_s.rjust(8,' ')}".ljust(10).magenta puts "#{obj.type}".yellow } puts "---------- Trailer ---------".white.bold if not @trailer.dictionary puts " [x] No trailer found.".blue else @trailer.dictionary.each_pair { |entry, value| print " [*] ".magenta print "#{entry.to_s}: ".yellow puts "#{value.to_s}".red } print " [+] ".magenta print "startxref: ".yellow puts "#{@trailer.startxref}".red end end def inspect to_s end end def to_s puts puts "---------- Header ----------".white.bold print " [+] ".magenta print "Version: ".yellow puts "#{@header.major_version}.#{@header.minor_version}".red @revisions.each { |revision| revision.to_s } puts end def inspect to_s end end end origami-2.0.0/bin/pdfextract0000755000004100000410000002014712757133666016056 0ustar www-datawww-data#!/usr/bin/env ruby =begin = Info Extracts valuable data from a PDF document. Can extract: - decoded streams - JavaScript - file attachments = License Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end begin require 'origami' rescue LoadError $: << File.join(__dir__, '../lib') require 'origami' end include Origami require 'optparse' require 'rexml/document' class OptParser BANNER = < [-afjms] [-d ] Extracts various data out of a document (streams, scripts, images, fonts, metadata, attachments). Bug reports or feature requests at: http://github.com/gdelugre/origami Options: USAGE def self.parser(options) OptionParser.new do |opts| opts.banner = BANNER opts.on("-d", "--output-dir DIR", "Output directory") do |d| options[:output_dir] = d end opts.on("-s", "--streams", "Extracts all decoded streams") do options[:streams] = true end opts.on("-a", "--attachments", "Extracts file attachments") do options[:attachments] = true end opts.on("-f", "--fonts", "Extracts embedded font files") do options[:fonts] = true end opts.on("-j", "--js", "Extracts JavaScript scripts") do options[:javascript] = true end opts.on("-m", "--metadata", "Extracts metadata streams") do options[:metadata] = true end opts.on("-i", "--images", "Extracts embedded images") do options[:images] = true end opts.on_tail("-h", "--help", "Show this message") do puts opts exit end end end def self.parse(args) options = {} self.parser(options).parse!(args) options end end begin @options = OptParser.parse(ARGV) if ARGV.empty? abort "Error: No filename was specified. #{$0} --help for details." else target = ARGV.shift end unless %i[streams javascript attachments fonts metadata images].any? {|opt| @options[opt]} @options[:streams] = @options[:javascript] = @options[:fonts] = @options[:attachments] = @options[:images] = true end if @options[:output_dir].nil? @options[:output_dir] = "#{File.basename(target, '.pdf')}.dump" end # Force data extraction, even for invalid FlateDecode streams. Origami::OPTIONS[:ignore_zlib_errors] = true Origami::OPTIONS[:ignore_png_errors] = true OUTPUT_DIR = @options[:output_dir] Dir::mkdir(OUTPUT_DIR) unless File.directory?(OUTPUT_DIR) params = { verbosity: Parser::VERBOSE_QUIET, } pdf = PDF.read(target, params) if @options[:streams] nstreams = 0 stream_dir = File.join(OUTPUT_DIR, "streams") Dir::mkdir(stream_dir) unless File.directory?(stream_dir) pdf.root_objects.find_all{|obj| obj.is_a?(Stream)}.each do |stream| stream_file = File.join(stream_dir, "stream_#{stream.reference.refno}.dmp") File.binwrite(stream_file, stream.data) nstreams += 1 end puts "Extracted #{nstreams} PDF streams to '#{stream_dir}'." end if @options[:javascript] nscripts = 0 js_dir = File.join(OUTPUT_DIR, "scripts") Dir::mkdir(js_dir) unless File.directory?(js_dir) pdf.ls(/^JS$/).each do |script| script_file = File.join(js_dir, "script_#{script.hash}.js") script_data = case script when Stream then script.data else script.value end File.binwrite(script_file, script_data) nscripts += 1 end # Also checking for presence of JavaScript in XML forms. if pdf.form? and pdf.Catalog.AcroForm.has_key?(:XFA) xfa = pdf.Catalog.AcroForm[:XFA].solve case xfa when Array then xml = "" i = 0 xfa.each do |packet| if i % 2 == 1 xml << packet.solve.data end i = i + 1 end when Stream then xml = xfa.data else reject("Malformed XFA dictionary") end xfadoc = REXML::Document.new(xml) REXML::XPath.match(xfadoc, "//script").each do |script| script_file = File.join(js_dir, "script_#{script.hash}.js") File.binwrite(script_file, script.text) nscripts += 1 end end puts "Extracted #{nscripts} scripts to '#{js_dir}'." end if @options[:attachments] nattach = 0 attachments_dir = File.join(OUTPUT_DIR, "attachments") Dir::mkdir(attachments_dir) unless File.directory?(attachments_dir) pdf.each_attachment do |name, attachment| attached_file = File.join(attachments_dir, "attached_#{File.basename(name)}") spec = attachment.solve if spec and spec.EF and f = spec.EF.F and f.is_a?(Stream) File.binwrite(attached_file, f.data) nattach += 1 end end puts "Extracted #{nattach} attachments to '#{attachments_dir}'." end if @options[:fonts] nfonts = 0 fonts_dir = File.join(OUTPUT_DIR, "fonts") Dir::mkdir(fonts_dir) unless File.directory?(fonts_dir) pdf.root_objects.find_all{|obj| obj.is_a?(Stream)}.each do |stream| font = stream.xrefs.find{|obj| obj.is_a?(FontDescriptor)} if font font_file = File.join(fonts_dir, File.basename(font.FontName.value.to_s)) File.binwrite(font_file, stream.data) nfonts += 1 end end puts "Extracted #{nfonts} fonts to '#{fonts_dir}'." end if @options[:metadata] nmeta = 0 metadata_dir = File.join(OUTPUT_DIR, "metadata") Dir::mkdir(metadata_dir) unless File.directory?(metadata_dir) pdf.root_objects.find_all{|obj| obj.is_a?(MetadataStream)}.each do |stream| metadata_file = File.join(metadata_dir, "metadata_#{stream.reference.refno}.xml") File.binwrite(metadata_file, stream.data) nmeta += 1 end puts "Extracted #{nmeta} metadata streams to '#{metadata_dir}'." end if @options[:images] nimages = 0 image_dir = File.join(OUTPUT_DIR, "images") Dir::mkdir(image_dir) unless File.directory?(image_dir) pdf.root_objects.find_all{|obj| obj.is_a?(Graphics::ImageXObject)}.each do |stream| begin ext, image_data = stream.to_image_file image_file = File.join(image_dir, "image_#{stream.reference.refno}.#{ext}") if ext != 'png' and stream.ColorSpace == Graphics::Color::Space::DEVICE_CMYK STDERR.puts "Warning: file '#{image_file}' is intended to be viewed in CMYK color space." end File.binwrite(image_file, image_data) nimages += 1 rescue STDERR.puts "Unable to decode image (stream #{stream.reference.refno}). #{$!.message}" end end puts "Extracted #{nimages} images to '#{image_dir}'." end rescue abort "#{$!.class}: #{$!.message}" end origami-2.0.0/bin/pdfdecrypt0000755000004100000410000000427512757133666016062 0ustar www-datawww-data#!/usr/bin/env ruby =begin = Info Decrypts a PDF document. = License Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end begin require 'origami' rescue LoadError $: << File.join(__dir__, '../lib') require 'origami' end include Origami require 'optparse' class OptParser BANNER = <] [-p ] [-o ] Decrypts a PDF document. Supports RC4 40 to 128 bits, AES128, AES256. Bug reports or feature requests at: http://github.com/gdelugre/origami Options: USAGE def self.parser(options) OptionParser.new do |opts| opts.banner = BANNER opts.on("-o", "--output FILE", "Output PDF file (stdout by default)") do |o| options[:output] = o end opts.on("-p", "--password PASSWORD", "Password of the document") do |p| options[:password] = p end opts.on_tail("-h", "--help", "Show this message") do puts opts exit end end end def self.parse(args) options = { output: STDOUT, password: '' } self.parser(options).parse!(args) options end end begin @options = OptParser.parse(ARGV) target = (ARGV.empty?) ? STDIN : ARGV.shift params = { verbosity: Parser::VERBOSE_QUIET, password: @options[:password] } PDF.read(target, params).save(@options[:output], decrypt: true, noindent: true) rescue abort "#{$!.class}: #{$!.message}" end origami-2.0.0/bin/pdf2pdfa0000755000004100000410000000420412757133666015374 0ustar www-datawww-data#!/usr/bin/env ruby =begin = Info Enforces a document to be rendered as PDF/A. This will disable multimedia features and JavaScript execution in Adobe Reader. = License Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end begin require 'origami' rescue LoadError $: << File.join(__dir__, '../lib') require 'origami' end include Origami require 'optparse' class OptParser BANNER = <] [-o ] Enforces a document to be rendered as PDF/A. This will disable multimedia features and JavaScript execution in Adobe Reader. Bug reports or feature requests at: http://github.com/gdelugre/origami Options: USAGE def self.parser(options) OptionParser.new do |opts| opts.banner = BANNER opts.on("-o", "--output FILE", "Output PDF file (stdout by default)") do |o| options[:output] = o end opts.on_tail("-h", "--help", "Show this message") do puts opts exit end end end def self.parse(args) options = { output: STDOUT, } self.parser(options).parse!(args) options end end begin @options = OptParser.parse(ARGV) target = (ARGV.empty?) ? STDIN : ARGV.shift params = { verbosity: Parser::VERBOSE_QUIET, } PDF.read(target, params).save(@options[:output], intent: 'PDF/A', noindent: true) rescue abort"#{$!.class}: #{$!.message}" end origami-2.0.0/bin/pdf2ruby0000755000004100000410000002161412757133666015447 0ustar www-datawww-data#!/usr/bin/env ruby =begin = Info Convert a PDF document to an Origami script. Experimental. = License: Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end require 'optparse' require 'fileutils' require 'colorize' begin require 'origami' rescue LoadError $: << File.join(__dir__, '../lib') require 'origami' end include Origami @var_hash = {} @code_hash = {} @obj_route = [] @current_idx = nil class OptParser def self.parse(args) options = {} options[:verbose] = options[:xstreams] = false opts = OptionParser.new do |opts| opts.banner = < Convert a PDF document to an Origami script (experimental). Options: BANNER opts.on("-v", "--verbose", "Verbose mode") do options[:verbose] = true end opts.on("-x", "--extract-streams", "Extract PDF streams to separate files") do options[:xstreams] = true end opts.on_tail("-h", "--help", "Show this message") do puts opts exit end end opts.parse!(args) options end end @options = OptParser.parse(ARGV) if ARGV.empty? abort "Error: No filename was specified. #{$0} --help for details." else TARGET = ARGV.shift end Origami::OPTIONS[:enable_type_guessing] = Origami::OPTIONS[:enable_type_propagation] = true TARGET_DIR = File.basename(TARGET, '.pdf') TARGET_FILE = File.join(TARGET_DIR, "#{TARGET_DIR}.rb") STREAM_DIR = "streams" def objectToRuby(obj, inclevel = 0, internalname = nil, do_convert = false) code = "" code << case obj when Origami::Null "Null.new" when Origami::Boolean, Origami::Number obj.value.to_s when Origami::String obj.inspect when Origami::Dictionary customclass = nil if obj.class != Origami::Dictionary p = (obj.class == Origami::Encoding) ? 0 : 1 customclass = obj.class.to_s.split('::')[p..-1].join('::') # strip Origami prefix if there is no collision end dictionaryToRuby(obj, inclevel, internalname, customclass) when Origami::Array arrayToRuby(obj, inclevel, internalname) when Origami::Stream streamToRuby(obj, internalname) unless obj.is_a?(ObjectStream) or obj.is_a?(XRefStream) when Origami::Name nameToRuby(obj) when Origami::Reference referenceToRuby(obj, internalname) else raise RuntimeError, "Unknown object type: #{obj.class}" end case obj when Origami::String, Origami::Dictionary, Origami::Array, Origami::Name code << ".to_o" if do_convert end code end def referenceToRuby(ref, internalname) varname = @var_hash[ref] if varname.nil? "nil" elsif @obj_route[0..@current_idx].include?(varname) @code_hash[varname] ||= {} @code_hash[varname][:afterDecl] ||= [] @code_hash[varname][:afterDecl] << "#{internalname} = #{varname}"#.to_o.set_indirect(true)" "nil" else @obj_route.push(varname) unless @obj_route.include?(varname) varname end end def nameToRuby(name) code = ':' valid = (name.value.to_s =~ /[+.:-]/).nil? code << '"' unless valid code << name.value.to_s code << '"' unless valid code end def arrayToRuby(arr, inclevel, internalname) i = 0 code = "\n" + " " * inclevel + "[" arr.each do |obj| subintname = "#{internalname}[#{i}]" code << "#{objectToRuby(obj, inclevel + 1, subintname)}" code << ", " unless i == arr.length - 1 i = i + 1 end code << "]" code end def dictionaryToRuby(dict, inclevel, internalname, customtype = nil) i = 0 code = "\n" + " " * inclevel if customtype code << "#{customtype}.new(#{dictionaryToHashMap(dict, inclevel, internalname)}" code << " " * inclevel + ")" else code << "{\n" dict.each_pair do |key, val| rubyname = nameToRuby(key) subintname = "#{internalname}[#{rubyname}]" if val.is_a?(Origami::Reference) and @var_hash[val] and @var_hash[val][0,3] == "obj" oldname = @var_hash[val] newname = (key.value.to_s.downcase.gsub(/[^[[:alnum:]]]/,'_') + "_" + @var_hash[val][4..-1]).gsub('.','_') if not @obj_route.include?(oldname) @var_hash[val] = newname @code_hash[newname] = @code_hash[oldname] @code_hash.delete(oldname) end end code << " " * (inclevel + 1) + "#{rubyname} => #{objectToRuby(val, inclevel + 2, subintname)}" code << ", " unless i == dict.length - 1 i = i + 1 code << "\n" end code << " " * inclevel + "}" end code end def dictionaryToHashMap(dict, inclevel, internalname) i = 0 code = "\n" dict.each_pair do |key, val| rubyname = nameToRuby(key) subintname = "#{internalname}[#{rubyname}]" if val.is_a?(Origami::Reference) and @var_hash[val] and @var_hash[val][0,3] == "obj" oldname = @var_hash[val] newname = (key.value.to_s.downcase + "_" + @var_hash[val][4..-1]).gsub('.','_') if not @obj_route.include?(oldname) @var_hash[val] = newname @code_hash[newname] = @code_hash[oldname] @code_hash.delete(oldname) end end code << " " * (inclevel + 1) + "#{rubyname} => #{objectToRuby(val, inclevel + 2, subintname)}" code << ", " unless i == dict.length - 1 i = i + 1 code << "\n" end code end def streamToRuby(stm, internalname) dict = stm.dictionary.dup.delete_if{|k,v| k == :Length} code = "Stream.new(" if @options[:xstreams] stmdir = File.join(TARGET_DIR, STREAM_DIR) Dir::mkdir(stmdir) unless File.directory? stmdir stmfile = File.join(stmdir, "stm_#{stm.reference.refno}.data") File.binwrite(stmfile, stm.data) code << "File.binread('#{stmfile}')" else code << stm.data.inspect << ".b" end code << ", #{dictionaryToHashMap(dict, 1, internalname)}" unless dict.empty? code << ")" code end puts "[*] ".red + "Loading document '#{TARGET}'" verbosity = @options[:verbose] ? Parser::VERBOSE_TRACE : Parser::VERBOSE_QUIET target = PDF.read(TARGET, verbosity: verbosity) puts "[*] ".red + "Document successfully loaded into Origami" Dir::mkdir(TARGET_DIR) unless File.directory? TARGET_DIR fd = File.open(TARGET_FILE, 'w', 0700) DOCREF = "pdf" fd.puts < 200, :y => 750, :rendering => Text::Rendering::FILL, :size => 30 contents.write "The script first tries to run your browser using JavaScript.", :x => 100, :y => 670, :size => 15 # A JS script to execute at the opening of the document jscript = < Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. origami-2.0.0/lib/0000755000004100000410000000000012757133666013756 5ustar www-datawww-dataorigami-2.0.0/lib/origami.rb0000644000004100000410000000344712757133666015742 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end module Origami # # Global options for Origami. # OPTIONS = { enable_type_checking: true, # set to false to disable type consistency checks during compilation. enable_type_guessing: true, # set to false to prevent the parser to guess the type of special dictionary and streams (not recommended). enable_type_propagation: true, # set to false to prevent the parser to propagate type from parents to children. use_openssl: true, # set to false to use Origami crypto backend. ignore_bad_references: false, # set to interpret invalid references as Null objects, instead of raising an exception. ignore_zlib_errors: false, # set to true to ignore exceptions on invalid Flate streams. ignore_png_errors: false, # set to true to ignore exceptions on invalid PNG predictors. } autoload :FDF, 'origami/extensions/fdf' autoload :PPKLite, 'origami/extensions/ppklite' end require 'origami/version' require 'origami/pdf' origami-2.0.0/lib/origami/0000755000004100000410000000000012757133666015405 5ustar www-datawww-dataorigami-2.0.0/lib/origami/export.rb0000644000004100000410000002206212757133666017255 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end module Origami class PDF # # Exports the document to a dot Graphiz file. # _filename_:: The path where to save the file. # def export_to_graph(path) appearance = -> (object) do label = object.type.to_s case object when Catalog fontcolor = "red" color = "mistyrose" shape = "ellipse" when Name, Number label = object.value fontcolor = "brown" color = "lightgoldenrodyellow" shape = "polygon" when String label = object.value if (object.ascii_only? and object.length <= 50) fontcolor = "red" color = "white" shape = "polygon" when Array fontcolor = "darkgreen" color = "lightcyan" shape = "ellipse" else fontcolor = "blue" color = "aliceblue" shape = "ellipse" end { label: label, fontcolor: fontcolor, color: color, shape: shape } end add_edges = -> (fd, object) do if object.is_a?(Array) or object.is_a?(ObjectStream) object.each do |subobj| fd << "\t#{object.object_id} -> #{subobj.solve.object_id}\n" end elsif object.is_a?(Dictionary) object.each_pair do |name, subobj| fd << "\t#{object.object_id} -> #{subobj.solve.object_id} " fd << "[label=\"#{name.value}\",fontsize=9];\n" end end if object.is_a?(Stream) object.dictionary.each_pair do |key, value| fd << "\t#{object.object_id} -> #{value.solve.object_id} " fd << "[label=\"#{key.value}\",fontsize=9];\n" end end end graph_name = "PDF" if graph_name.nil? or graph_name.empty? fd = File.open(path, "w") begin fd << "digraph #{graph_name} {\n\n" objects = self.objects(include_keys: false).find_all{ |obj| not obj.is_a?(Reference) } objects.each do |object| attr = appearance[object] fd << "\t#{object.object_id} " fd << "[label=\"#{attr[:label]}\",shape=#{attr[:shape]},color=#{attr[:color]},style=filled,fontcolor=#{attr[:fontcolor]},fontsize=16];\n" if object.is_a?(Stream) object.dictionary.each do |value| unless value.is_a?(Reference) attr = appearance[value] fd << "\t#{value.object_id} " fd << "[label=\"#{attr[:label]}\",shape=#{attr[:shape]},color=#{attr[:color]},style=filled,fontcolor=#{attr[:fontcolor]},fontsize=16];\n" end end end add_edges.call(fd, object) end fd << "\n}" ensure fd.close end end # # Exports the document to a GraphML file. # _filename_:: The path where to save the file. # def export_to_graphml(path) require 'rexml/document' declare_node = -> (id, attr) do <<-XML #{attr[:label]} XML end declare_edge = -> (id, src, dest, label = nil) do <<-XML #{label.to_s} XML end appearance = -> (object) do label = object.type.to_s case object when Catalog fontcolor = "red" color = "mistyrose" shape = "doublecircle" when Name, Number label = object.value fontcolor = "orange" color = "lightgoldenrodyellow" shape = "polygon" when String label = object.value if (object.ascii_only? and object.length <= 50) fontcolor = "red" color = "white" shape = "polygon" when Array fontcolor = "green" color = "lightcyan" shape = "ellipse" else fontcolor = "blue" color = "aliceblue" shape = "ellipse" end { label: label, fontcolor: fontcolor, color: color, shape: shape } end add_edges = -> (xml, object, id) do if object.is_a?(Array) or object.is_a?(ObjectStream) object.each do |subobj| xml << declare_edge["e#{id}", "n#{object.object_id}", "n#{subobj.solve.object_id}"] id = id + 1 end elsif object.is_a?(Dictionary) object.each_pair do |name, subobj| xml << declare_edge["e#{id}", "n#{object.object_id}", "n#{subobj.solve.object_id}", name.value] id = id + 1 end end if object.is_a?(Stream) object.dictionary.each_pair do |key, value| xml << declare_edge["e#{id}", "n#{object.object_id}", "n#{value.object_id}", key.value] id = id + 1 end end id end graph_name = "PDF" if graph_name.nil? or graph_name.empty? edge_nb = 1 xml = <<-XML XML objects = self.objects(include_keys: false).find_all{ |obj| not obj.is_a?(Reference) } objects.each do |object| xml << declare_node["n#{object.object_id}", appearance[object]] if object.is_a?(Stream) object.dictionary.each do |value| unless value.is_a?(Reference) xml << declare_node[value.object_id, appearance[value]] end end end edge_nb = add_edges[xml, object, edge_nb] end xml << '' << "\n" xml << '' doc = REXML::Document.new(xml) formatter = REXML::Formatters::Pretty.new(4) formatter.compact = true File.open(path, "w") do |fd| formatter.write(doc, fd) end end end end origami-2.0.0/lib/origami/collections.rb0000644000004100000410000001272412757133666020256 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end module Origami class PDF # # Returns true if the document behaves as a portfolio for embedded files. # def portfolio? self.Catalog.Collection.is_a?(Dictionary) end end class Collection < Dictionary include StandardObject module View DETAILS = :D TILE = :T HIDDEN = :H end class Schema < Dictionary include StandardObject field :Type, :Type => Name, :Default => :CollectionSchema end class Navigator < Dictionary include StandardObject module Type FLEX = :Module FLASH = :Default end field :SWF, :Type => String, :Required => true field :Name, :Type => String, :Required => true field :Desc, :Type => String field :Category, :Type => String field :ID, :Type => String, :Required => true field :Version, :Type => String field :APIVersion, :Type => String, :Required => true field :LoadType, :Type => Name, :Default => Type::FLASH field :Icon, :Type => String field :Locale, :Type => String field :Strings, :Type => NameTreeNode.of(String) field :InitialFields, :Type => Schema field :Resources, :Type => NameTreeNode.of(Stream), :Required => true end class Color < Dictionary include StandardObject field :Background, :Type => Array.of(Number, length: 3) field :CardBackground, :Type => Array.of(Number, length: 3) field :CardBorder, :Type => Array.of(Number, length: 3) field :PrimaryText, :Type => Array.of(Number, length: 3) field :SecondaryText, :Type => Array.of(Number, length: 3) end class Split < Dictionary include StandardObject HORIZONTAL = :H VERTICAL = :V NONE = :N field :Direction, :Type => Name field :Position, :Type => Number end class Item < Dictionary include StandardObject field :Type, :Type => Name, :Default => :CollectionItem end class Subitem < Dictionary include StandardObject field :Type, :Type => Name, :Default => :CollectionSubitem field :D, :Type => [ String, Number ] field :P, :Type => String end class Folder < Dictionary include StandardObject field :Type, :Type => Name, :Default => :Folder field :ID, :Type => Integer, :Required => true field :Name, :Type => String, :Required => true field :Parent, :Type => Folder field :Child, :Type => Folder field :Next, :Type => Folder field :CI, :Type => Item field :Desc, :Type => String field :CreationDate, :Type => String field :ModDate, :Type => String field :Thumb, :Type => Stream field :Free, :Type => Array.of(Array.of(Integer, length: 2)) end class Sort < Dictionary include StandardObject field :Type, :Type => Name, :Default => :CollectionSort field :S, :Type => [ Name, Array.of(Name) ] field :A, :Type => [ Boolean, Array.of(Boolean) ] end # # Collection fields. # field :Type, :Type => Name, :Default => :Collection field :Schema, :Type => Schema field :D, :Type => String field :View, :Type => Name, :Default => View::DETAILS field :Sort, :Type => Sort field :Navigator, :Type => Navigator, :ExtensionLevel => 3 field :Resources, :Type => NameTreeNode.of(Stream), :ExtensionLevel => 3 field :Colors, :Type => Color, :ExtensionLevel => 3 field :Folders, :Type => Folder, :ExtensionLevel => 3 field :Split, :Type => Split, :ExtensionLevel => 3 end end origami-2.0.0/lib/origami/xfa.rb0000644000004100000410000031615112757133666016517 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end require 'rexml/document' module Origami class XFAStream < Stream # TODO end class PDF def create_xfa_form(xdp, *fields) acroform = create_form(*fields) acroform.XFA = XFAStream.new(xdp, :Filter => :FlateDecode) acroform end def xfa_form? self.form? and self.Catalog.AcroForm.key?(:XFA) end end module XFA class XFAError < Error #:nodoc: end module ClassMethods def xfa_attribute(name) # Attribute getter. attr_getter = "attr_#{name.to_s}" remove_method(attr_getter) rescue NameError define_method(attr_getter) do self.attributes[name.to_s] end # Attribute setter. attr_setter = "attr_#{name.to_s}=" remove_method(attr_setter) rescue NameError define_method(attr_setter) do |value| self.attributes[names.to_s] = value end end def xfa_node(name, type, range = (0..(1.0/0))) adder = "add_#{name}" remove_method(adder) rescue NameError define_method(adder) do |*attr| elt = self.add_element(type.new) unless attr.empty? attr.first.each do |k,v| elt.attributes[k.to_s] = v end end elt end end def mime_type(type) define_method("mime_type") { return type } end end def self.included(receiver) receiver.extend(ClassMethods) end class Element < REXML::Element include XFA end end module XDP module Packet # # This packet encloses the configuration settings. # class Config < XFA::Element mime_type 'text/xml' def initialize super("config") add_attribute 'xmlns:xfa', 'http://www.xfa.org/schema/xci/3.0/' end class URI < XFA::Element xfa_attribute 'desc' xfa_attribute 'lock' def initialize(uri = "") super('uri') self.text = uri end end class Debug < XFA::Element xfa_attribute 'desc' xfa_attribute 'lock' xfa_node 'uri', Config::URI, 0..1 def initialize super('debug') end end class AdjustData < XFA::Element xfa_attribute 'desc' xfa_attribute 'lock' def initialize(coercion = "0") super('adjustData') self.text = coercion end end class Attributes < XFA::Element xfa_attribute 'desc' xfa_attribute 'lock' PRESERVE = "preserve" DELEGATE = "delegate" IGNORE = "ignore" def initialize(attr = PRESERVE) super('attributes') self.text = attr end end class IncrementalLoad < XFA::Element xfa_attribute 'desc' xfa_attribute 'lock' NONE = "none" FORWARDONLY = "forwardOnly" def initialize(incload = NONE) super('incrementalLoad') self.text = incload end end class Locale < XFA::Element xfa_attribute 'desc' xfa_attribute 'lock' def initialize(locale = "") super('locale') self.text = locale end end class LocaleSet < XFA::Element xfa_attribute 'desc' xfa_attribute 'lock' def initialize(uri = "") super('localeSet') self.text = uri end end class OutputXSL < XFA::Element xfa_attribute 'desc' xfa_attribute 'lock' xfa_node 'uri', Config::URI, 0..1 def initialize super('outputXSL') end end class Range < XFA::Element xfa_attribute 'desc' xfa_attribute 'lock' def initialize(range = "") super('range') self.text = range end end class Record < XFA::Element xfa_attribute 'desc' xfa_attribute 'lock' def initialize(record = "") super('record') self.text = "" end end class StartNode < XFA::Element xfa_attribute 'desc' xfa_attribute 'lock' def initialize(somexpr = "") super('startNode') self.text = somexpr end end class Window < XFA::Element xfa_attribute 'desc' xfa_attribute 'lock' def initialize(win = "0") super('window') self.text = win end end class XSL < XFA::Element xfa_attribute 'desc' xfa_attribute 'lock' xfa_node 'debug', Config::Debug, 0..1 xfa_node 'uri', Config::URI, 0..1 def initialize super('xsl') end end class ExcludeNS < XFA::Element xfa_attribute 'desc' xfa_attribute 'lock' def initialize(ns = "") super('excludeNS') self.text = ns end end class GroupParent < XFA::Element xfa_attribute 'desc' xfa_attribute 'lock' def initialize(parentname = "") super('groupParent') self.text = parentname end end class IfEmpty < XFA::Element xfa_attribute 'desc' xfa_attribute 'lock' DATAVALUE = "dataValue" DATAGROUP = "dataGroup" IGNORE = "ignore" REMOVE = "remove" def initialize(default = DATAVALUE) super('ifEmpty') self.text = default end end class NameAttr < XFA::Element xfa_attribute 'desc' xfa_attribute 'lock' def initialize(name) super('nameAttr') self.text = name end end class Picture < XFA::Element xfa_attribute 'desc' xfa_attribute 'lock' def initialize(clause = "") super('picture') self.text = clause end end class Presence < XFA::Element xfa_attribute 'desc' xfa_attribute 'lock' PRESERVE = "preserve" DISSOLVE = "dissolve" DISSOLVESTRUCTURE = "dissolveStructure" IGNORE = "ignore" REMOVE = "remove" def initialize(action = PRESERVE) super('presence') self.text = action end end class Rename < XFA::Element xfa_attribute 'desc' xfa_attribute 'lock' def initialize(nodename = "") super('rename') self.text = nodename end end class Whitespace < XFA::Element xfa_attribute 'desc' xfa_attribute 'lock' PRESERVE = "preserve" LTRIM = "ltrim" NORMALIZE = "normalize" RTRIM = "rtrim" TRIM = "trim" def initialize(action = PRESERVE) super('whitespace') self.text = action end end class Transform < XFA::Element xfa_attribute 'desc' xfa_attribute 'lock' xfa_attribute 'ref' xfa_node 'groupParent', Config::GroupParent, 0..1 xfa_node 'ifEmpty', Config::IfEmpty, 0..1 xfa_node 'nameAttr', Config::NameAttr, 0..1 xfa_node 'picture', Config::Picture, 0..1 xfa_node 'presence', Config::Presence, 0..1 xfa_node 'rename', Config::Rename, 0..1 xfa_node 'whitespace', Config::Whitespace, 0..1 end class Data < XFA::Element xfa_attribute 'desc' xfa_attribute 'lock' xfa_node 'adjustData', Config::AdjustData, 0..1 xfa_node 'attributes', Config::Attributes, 0..1 xfa_node 'incrementalLoad', Config::IncrementalLoad, 0..1 xfa_node 'outputXSL', Config::OutputXSL, 0..1 xfa_node 'range', Config::Range, 0..1 xfa_node 'record', Config::Record, 0..1 xfa_node 'startNode', Config::StartNode, 0..1 xfa_node 'uri', Config::URI, 0..1 xfa_node 'window', Config::Window, 0..1 xfa_node 'xsl', Config::XSL, 0..1 xfa_node 'excludeNS', Config::ExcludeNS xfa_node 'transform', Config::Transform def initialize super('data') end end class Severity < XFA::Element xfa_attribute 'desc' xfa_attribute 'lock' IGNORE = "ignore" ERROR = "error" INFORMATION = "information" TRACE = "trace" WARNING = "warning" def initialize(level = IGNORE) super('severity') self.text = level end end class MsgId < XFA::Element xfa_attribute 'desc' xfa_attribute 'lock' def initialize(uid = "1") super('msgId') self.text = uid end end class Message < XFA::Element xfa_attribute 'desc' xfa_attribute 'lock' xfa_node 'msgId', Config::MsgId, 0..1 xfa_node 'severity', Config::Severity, 0..1 def initialize super('message') end end class Messaging < XFA::Element xfa_attribute 'desc' xfa_attribute 'lock' xfa_node 'message', Config::Message def initialize super('messaging') end end class SuppressBanner < XFA::Element xfa_attribute 'desc' xfa_attribute 'lock' ALLOWED = "0" DENIED = "1" def initialize(display = ALLOWED) super('suppressBanner') self.text = display end end class Base < XFA::Element xfa_attribute 'desc' xfa_attribute 'lock' def initialize(uri = "") super('base') self.text = uri end end class Relevant < XFA::Element xfa_attribute 'desc' xfa_attribute 'lock' def initialize(token = "") super('relevant') self.text = token end end class StartPage < XFA::Element xfa_attribute 'desc' xfa_attribute 'lock' def initialize(pagenum = "0") super('startPage') self.text = pagenum end end class Template < XFA::Element xfa_attribute 'desc' xfa_attribute 'lock' xfa_node 'base', Config::Base, 0..1 xfa_node 'relevant', Config::Relevant, 0..1 xfa_node 'startPage', Config::StartPage, 0..1 xfa_node 'uri', Config::URI, 0..1 xfa_node 'xsl', Config::XSL, 0..1 def initialize super('template') end end class ValidationMessaging < XFA::Element xfa_attribute 'desc' xfa_attribute 'lock' ALL_INDIVIDUALLY = "allMessagesIndividually" ALL_TOGETHER = "allMessagesTogether" FIRST_ONLY = "firstMessageOnly" NONE = "noMessages" def initialize(validate = ALL_INDIVIDUALLY) super('validationMessaging') self.text = validate end end class VersionControl < XFA::Element xfa_attribute 'lock' xfa_attribute 'outputBelow' xfa_attribute 'sourceAbove' xfa_attribute 'sourceBelow' def initialize super('versionControl') end end class Mode < XFA::Element xfa_attribute 'desc' xfa_attribute 'lock' APPEND = "append" OVERWRITE = "overwrite" def initialize(mode = APPEND) super('mode') self.text = mode end end class Threshold < XFA::Element xfa_attribute 'desc' xfa_attribute 'lock' TRACE = "trace" ERROR = "error" INFORMATION = "information" WARN = "warn" def initialize(threshold = TRACE) super('threshold') self.text = threshold end end class To < XFA::Element xfa_attribute 'desc' xfa_attribute 'lock' NULL = "null" MEMORY = "memory" STD_ERR = "stderr" STD_OUT = "stdout" SYSTEM = "system" URI = "uri" def initialize(dest = NULL) super('to') self.text = dest end end class Log < XFA::Element xfa_attribute 'desc' xfa_attribute 'lock' xfa_node 'mode', Config::Mode, 0..1 xfa_node 'threshold', Config::Threshold, 0..1 xfa_node 'to', Config::To, 0..1 xfa_node 'uri', Config::URI, 0..1 def initialize super('log') end end class Common < XFA::Element xfa_attribute 'desc' xfa_attribute 'lock' xfa_node 'data', Config::Data, 0..1 xfa_node 'locale', Config::Locale, 0..1 xfa_node 'localeSet', Config::LocaleSet, 0..1 xfa_node 'messaging', Config::Messaging, 0..1 xfa_node 'suppressBanner', Config::SuppressBanner, 0..1 xfa_node 'template', Config::Template, 0..1 xfa_node 'validationMessaging', Config::ValidationMessaging, 0..1 xfa_node 'versionControl', Config::VersionControl, 0..1 xfa_node 'log', Config::Log def initialize super("common") end end end # # The _connectionSet_ packet describes the connections used to initiate or conduct web services. # class ConnectionSet < XFA::Element mime_type 'text/xml' def initialize super("connectionSet") add_attribute 'xmlns', 'http://www.xfa.org/schema/xfa-connection-set/2.8/' end class EffectiveInputPolicy < XFA::Element xfa_attribute 'id' xfa_attribute 'name' xfa_attribute 'use' xfa_attribute 'usehref' def initialize super('effectiveInputPolicy') end end class EffectiveOutputPolicy < XFA::Element xfa_attribute 'id' xfa_attribute 'name' xfa_attribute 'use' xfa_attribute 'usehref' def initialize super('effectiveOutputPolicy') end end class Operation < XFA::Element xfa_attribute 'id' xfa_attribute 'input' xfa_attribute 'name' xfa_attribute 'output' xfa_attribute 'use' xfa_attribute 'usehref' def initialize(name = "") super('operation') self.text = name end end class SOAPAction < XFA::Element xfa_attribute 'id' xfa_attribute 'name' xfa_attribute 'use' xfa_attribute 'usehref' def initialize(uri = "") super('soapAction') self.text = uri end end class SOAPAddress < XFA::Element xfa_attribute 'id' xfa_attribute 'name' xfa_attribute 'use' xfa_attribute 'usehref' def initialize(addr = "") super('soapAddress') self.text = addr end end class WSDLAddress < XFA::Element xfa_attribute 'id' xfa_attribute 'name' xfa_attribute 'use' xfa_attribute 'usehref' def initialize(addr = "") super('wsdlAddress') self.text = addr end end class WSDLConnection < XFA::Element xfa_attribute 'dataDescription' xfa_attribute 'name' xfa_node 'effectiveInputPolicy', ConnectionSet::EffectiveInputPolicy, 0..1 xfa_node 'effectiveOutputPolicy', ConnectionSet::EffectiveOutputPolicy, 0..1 xfa_node 'operation', ConnectionSet::Operation, 0..1 xfa_node 'soapAction', ConnectionSet::SOAPAction, 0..1 xfa_node 'soapAddress', ConnectionSet::SOAPAddress, 0..1 xfa_node 'wsdlAddress', ConnectionSet::WSDLAddress, 0..1 def initialize super('wsdlConnection') end end class URI < XFA::Element xfa_attribute 'id' xfa_attribute 'name' xfa_attribute 'use' xfa_attribute 'usehref' def initialize(uri = "") super('uri') self.text = uri end end class RootElement < XFA::Element xfa_attribute 'id' xfa_attribute 'name' xfa_attribute 'use' xfa_attribute 'usehref' def initialize(root = '') super('rootElement') self.text = root end end class XSDConnection < XFA::Element xfa_attribute 'dataDescription' xfa_attribute 'name' xfa_node 'rootElement', ConnectionSet::RootElement, 0..1 xfa_node 'uri', ConnectionSet::URI, 0..1 def initialize super('xsdConnection') end end class XMLConnection < XFA::Element xfa_attribute 'dataDescription' xfa_attribute 'name' xfa_node 'uri', ConnectionSet::URI, 0..1 def initialize super('xmlConnection') end end xfa_node 'wsdlConnection', ConnectionSet::WSDLConnection xfa_node 'xmlConnection', ConnectionSet::XMLConnection xfa_node 'xsdConnection', ConnectionSet::XSDConnection end # # The _datasets_ element enclosed XML data content that may have originated from an XFA form and/or # may be intended to be consumed by an XFA form. # class Datasets < XFA::Element mime_type 'text/xml' class Data < XFA::Element def initialize super('xfa:data') end end def initialize super("xfa:datasets") add_attribute 'xmlns:xfa', 'http://www.xfa.org/schema/xfa-data/1.0/' end end # # The _localeSet_ packet encloses information about locales. # class LocaleSet < XFA::Element mime_type 'text/xml' def initialize super("localeSet") add_attribute 'xmlns', 'http://www.xfa.org/schema/xfa-locale-set/2.7/' end end # # An XDF _pdf_ element encloses a PDF packet. # class PDF < XFA::Element mime_type 'application/pdf' xfa_attribute :href def initialize super("pdf") add_attribute 'xmlns', 'http://ns.adobe.com/xdp/pdf/' end def enclose_pdf(pdfdata) require 'base64' b64data = Base64.encode64(pdfdata).chomp! doc = elements['document'] || add_element('document') chunk = doc.elements['chunk'] || doc.add_element('chunk') chunk.text = b64data self end def has_enclosed_pdf? chunk = elements['document/chunk'] not chunk.nil? and not chunk.text.nil? end def remove_enclosed_pdf elements.delete('document') if has_enclosed_pdf? end def enclosed_pdf return nil unless has_enclosed_pdf? require 'base64' Base64.decode64(elements['document/chunk'].text) end end # # The _signature_ packet encloses a detached digital signature. # class Signature < XFA::Element mime_type '' def initialize super("signature") add_attribute 'xmlns', 'http://www.w3.org/2000/09/xmldsig#' end end # # The _sourceSet_ packet contains ADO database queries, used to describe data # binding to ADO data sources. # class SourceSet < XFA::Element mime_type 'text/xml' def initialize super("sourceSet") add_attribute 'xmlns', 'http://www.xfa.org/schema/xfa-source-set/2.8/' end end # # The _stylesheet_ packet encloses a single XSLT stylesheet. # class StyleSheet < XFA::Element mime_type 'text/css' def initialize(id) super("xsl:stylesheet") add_attribute 'version', '1.0' add_attribute 'xmlns:xsl', 'http://www.w3.org/1999/XSL/Transform' add_attribute 'id', id.to_s end end # # This packet contains the form template. # class Template < XFA::Element mime_type 'application/x-xfa-template' class Boolean < XFA::Element xfa_attribute 'id' xfa_attribute 'name' xfa_attribute 'use' xfa_attribute 'usehref' NO = 0 YES = 1 def initialize(bool = nil) super('boolean') self.text = bool end end class Date < XFA::Element xfa_attribute 'id' xfa_attribute 'name' xfa_attribute 'use' xfa_attribute 'usehref' def initialize(date = nil) super('date') self.text = date end end class DateTime < XFA::Element xfa_attribute 'id' xfa_attribute 'name' xfa_attribute 'use' xfa_attribute 'usehref' def initialize(datetime = nil) super('dateTime') self.text = datetime end end class Decimal < XFA::Element xfa_attribute 'fracDigits' xfa_attribute 'id' xfa_attribute 'leadDigits' xfa_attribute 'name' xfa_attribute 'use' xfa_attribute 'usehref' def initialize(number = nil) super('decimal') self.text = number end end class ExData < XFA::Element xfa_attribute 'contentType' xfa_attribute 'href' xfa_attribute 'id' xfa_attribute 'maxLength' xfa_attribute 'name' xfa_attribute 'rid' xfa_attribute 'transferEncoding' xfa_attribute 'use' xfa_attribute 'usehref' def initialize(data = nil) super('exData') self.text = data end end class Float < XFA::Element xfa_attribute 'id' xfa_attribute 'name' xfa_attribute 'use' xfa_attribute 'usehref' def initialize(float = nil) super('float') self.text = float end end class Image < XFA::Element xfa_attribute 'aspect' xfa_attribute 'contentType' xfa_attribute 'href' xfa_attribute 'id' xfa_attribute 'name' xfa_attribute 'transferEncoding' xfa_attribute 'use' xfa_attribute 'usehref' def initialize(data = nil) super('image') self.text = data end end class Integer < XFA::Element xfa_attribute 'id' xfa_attribute 'name' xfa_attribute 'use' xfa_attribute 'usehref' def initialize(int = nil) super('integer') self.text = int end end class Text < XFA::Element xfa_attribute 'id' xfa_attribute 'maxChars' xfa_attribute 'name' xfa_attribute 'rid' xfa_attribute 'use' xfa_attribute 'usehref' def initialize(text = "") super('text') self.text = text end end class Time < XFA::Element xfa_attribute 'id' xfa_attribute 'name' xfa_attribute 'use' xfa_attribute 'usehref' def initialize(time = nil) super('time') self.text = time end end class Extras < XFA::Element xfa_attribute 'id' xfa_attribute 'name' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'boolean', Template::Boolean xfa_node 'date', Template::Date xfa_node 'dateTime', Template::DateTime xfa_node 'decimal', Template::Decimal xfa_node 'exData', Template::ExData xfa_node 'extras', Template::Extras xfa_node 'float', Template::Float xfa_node 'image', Template::Image xfa_node 'integer', Template::Integer xfa_node 'text', Template::Text xfa_node 'time', Template::Time def initialize super('extras') end end class Speak < XFA::Element xfa_attribute 'disable' xfa_attribute 'id' xfa_attribute 'priority' xfa_attribute 'rid' xfa_attribute 'use' xfa_attribute 'usehref' def initialize(text = "") super('speak') self.text = text end end class ToolTip < XFA::Element xfa_attribute 'id' xfa_attribute 'rid' xfa_attribute 'use' xfa_attribute 'usehref' def initialize(text = "") super('toolTip') end end class Assist < XFA::Element xfa_attribute 'id' xfa_attribute 'role' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'speak', Template::Speak, 0..1 xfa_node 'toolTip', Template::ToolTip, 0..1 def initialize super('assist') end end class Picture < XFA::Element xfa_attribute 'id' xfa_attribute 'use' xfa_attribute 'usehref' def initialize(data = nil) super('picture') self.text = data end end class Bind < XFA::Element xfa_attribute 'match' xfa_attribute 'ref' xfa_node 'picture', Template::Picture, 0..1 def initialize super('bind') end end class Bookend < XFA::Element xfa_attribute 'id' xfa_attribute 'leader' xfa_attribute 'trailer' xfa_attribute 'use' xfa_attribute 'usehref' def initialize super('bookend') end end class Color < XFA::Element xfa_attribute 'cSpace' xfa_attribute 'id' xfa_attribute 'use' xfa_attribute 'usehref' xfa_attribute 'value' xfa_node 'extras', Template::Extras, 0..1 def initialize super('color') self.cSpace = "SRGB" end end class Corner < XFA::Element xfa_attribute 'id' xfa_attribute 'inverted' xfa_attribute 'join' xfa_attribute 'presence' xfa_attribute 'radius' xfa_attribute 'stroke' xfa_attribute 'thickness' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'color', Template::Color, 0..1 xfa_node 'extras', Template::Extras, 0..1 def initialize super('corner') end end class Edge < XFA::Element xfa_attribute 'cap' xfa_attribute 'id' xfa_attribute 'presence' xfa_attribute 'stroke' xfa_attribute 'thickness' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'color', Template::Color, 0..1 xfa_node 'extras', Template::Extras, 0..1 def initialize super('edge') end end class Linear < XFA::Element xfa_attribute 'id' xfa_attribute 'type' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'color', Template::Color, 0..1 xfa_node 'extras', Template::Extras, 0..1 def initialize super('linear') end end class Pattern < XFA::Element xfa_attribute 'id' xfa_attribute 'type' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'color', Template::Color, 0..1 xfa_node 'extras', Template::Extras, 0..1 def initialize super('pattern') end end class Radial < XFA::Element xfa_attribute 'id' xfa_attribute 'type' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'color', Template::Color, 0..1 xfa_node 'extras', Template::Extras, 0..1 def initialize super('radial') end end class Solid < XFA::Element xfa_attribute 'id' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'extras', Template::Extras, 0..1 def initialize super('solid') end end class Stipple < XFA::Element xfa_attribute 'id' xfa_attribute 'rate' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'color', Template::Color, 0..1 xfa_node 'extras', Template::Extras, 0..1 def initialize super('stipple') end end class Fill < XFA::Element xfa_attribute 'id' xfa_attribute 'presence' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'color', Template::Color, 0..1 xfa_node 'extras', Template::Extras, 0..1 xfa_node 'linear', Template::Linear, 0..1 xfa_node 'pattern', Template::Pattern, 0..1 xfa_node 'radial', Template::Radial, 0..1 xfa_node 'solid', Template::Solid, 0..1 xfa_node 'stipple', Template::Stipple, 0..1 def initialize super('fill') end end class Margin < XFA::Element xfa_attribute 'bottomInset' xfa_attribute 'id' xfa_attribute 'leftInset' xfa_attribute 'rightInset' xfa_attribute 'topInset' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'extras', Template::Extras, 0..1 def initialize super('margin') end end class Border < XFA::Element xfa_attribute 'break' xfa_attribute 'hand' xfa_attribute 'id' xfa_attribute 'presence' xfa_attribute 'relevant' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'corner', Template::Corner, 0..4 xfa_node 'edge', Template::Edge, 0..4 xfa_node 'extras', Template::Extras, 0..1 xfa_node 'fill', Template::Fill, 0..1 xfa_node 'margin', Template::Margin, 0..1 def initialize super('border') end end class Break < XFA::Element xfa_attribute 'after' xfa_attribute 'afterTarget' xfa_attribute 'before' xfa_attribute 'beforeTarget' xfa_attribute 'bookendLeader' xfa_attribute 'bookendTrailer' xfa_attribute 'id' xfa_attribute 'overflowLeader' xfa_attribute 'overflowTarget' xfa_attribute 'overflowTrailer' xfa_attribute 'startNew' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'extras', Template::Extras, 0..1 def initialize super('break') end end class Message < XFA::Element xfa_attribute 'id' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'text', Template::Text def initialize super('message') end end class Script < XFA::Element xfa_attribute 'binding' xfa_attribute 'contentType' xfa_attribute 'id' xfa_attribute 'name' xfa_attribute 'runAt' xfa_attribute 'use' xfa_attribute 'usehref' def initialize(script = "") super('script') self.text = script end end class JavaScript < Script def initialize(script = "") super(script) self.contentType = 'application/x-javascript' end end class FormCalcScript < Script def initialize(script = "") super(script) self.contentType = 'application/x-formcalc' end end class Calculate < XFA::Element xfa_attribute 'id' xfa_attribute 'override' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'extras', Template::Extras, 0..1 xfa_node 'message', Template::Message, 0..1 xfa_node 'script', Template::Script, 0..1 def initialize super('calculate') end end class Desc < XFA::Element xfa_attribute 'id' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'boolean', Template::Boolean xfa_node 'date', Template::Date xfa_node 'dateTime', Template::DateTime xfa_node 'decimal', Template::Decimal xfa_node 'exData', Template::ExData xfa_node 'float', Template::Float xfa_node 'image', Template::Image xfa_node 'integer', Template::Integer xfa_node 'text', Template::Text xfa_node 'time', Template::Time def initialize super('desc') end end class Keep < XFA::Element xfa_attribute 'id' xfa_attribute 'intact' xfa_attribute 'next' xfa_attribute 'previous' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'extras', Template::Extras, 0..1 NONE = "none" CONTENTAREA = "contentArea" PAGEAREA = "pageArea" def initialize super('keep') end end class Occur < XFA::Element xfa_attribute 'id' xfa_attribute 'initial' xfa_attribute 'max' xfa_attribute 'min' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'extras', Template::Extras, 0..1 def initialize super('occur') end end class Overflow < XFA::Element xfa_attribute 'id' xfa_attribute 'leader' xfa_attribute 'target' xfa_attribute 'trailer' xfa_attribute 'use' xfa_attribute 'usehref' def initialize super('overflow') end end class Medium < XFA::Element xfa_attribute 'id' xfa_attribute 'imagingBBox' xfa_attribute 'long' xfa_attribute 'orientation' xfa_attribute 'short' xfa_attribute 'stock' xfa_attribute 'trayIn' xfa_attribute 'trayOut' xfa_attribute 'use' xfa_attribute 'usehref' def initialize super('medium') end end class Font < XFA::Element xfa_attribute 'baselineShift' xfa_attribute 'fontHorizontalScale' xfa_attribute 'fontVerticalScale' xfa_attribute 'id' xfa_attribute 'kerningMode' xfa_attribute 'letterSpacing' xfa_attribute 'lineThrough' xfa_attribute 'lineThroughPeriod' xfa_attribute 'overline' xfa_attribute 'overlinePeriod' xfa_attribute 'posture' xfa_attribute 'size' xfa_attribute 'typeface' xfa_attribute 'underline' xfa_attribute 'underlinePeriod' xfa_attribute 'use' xfa_attribute 'usehref' xfa_attribute 'weight' xfa_node 'extras', Template::Extras, 0..1 xfa_node 'fill', Template::Fill, 0..1 def initialize super('font') end end class Hyphenation < XFA::Element xfa_attribute 'excludeAllCaps' xfa_attribute 'excludeInitialCap' xfa_attribute 'hyphenate' xfa_attribute 'id' xfa_attribute 'pushCharacterCount' xfa_attribute 'remainCharacterCount' xfa_attribute 'use' xfa_attribute 'usehref' xfa_attribute 'wordCharacterCount' def initialize super('hyphenation') end end class Para < XFA::Element xfa_attribute 'hAlign' xfa_attribute 'id' xfa_attribute 'lineHeight' xfa_attribute 'marginLeft' xfa_attribute 'marginRight' xfa_attribute 'orphans' xfa_attribute 'preserve' xfa_attribute 'radixOffset' xfa_attribute 'spaceAbove' xfa_attribute 'spaceBelow' xfa_attribute 'tabDefault' xfa_attribute 'tabStops' xfa_attribute 'textIndent' xfa_attribute 'use' xfa_attribute 'usehref' xfa_attribute 'vAlign' xfa_attribute 'widows' xfa_node 'hyphenation', Template::Hyphenation, 0..1 def initialize super('para') end end class Arc < XFA::Element xfa_attribute 'circular' xfa_attribute 'hand' xfa_attribute 'id' xfa_attribute 'startAngle' xfa_attribute 'sweepAngle' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'edge', Template::Edge, 0..1 xfa_node 'fill', Template::Fill, 0..1 def initialize super('arc') end end class Line < XFA::Element xfa_attribute 'hand' xfa_attribute 'id' xfa_attribute 'slope' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'edge', Template::Edge, 0..1 def initialize super('line') end end class Rectangle < XFA::Element xfa_attribute 'hand' xfa_attribute 'id' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'corner', Template::Corner, 0..4 xfa_node 'edge', Template::Edge, 0..4 xfa_node 'fill', Template::Fill, 0..4 def initialize super('rectangle') end end class Value < XFA::Element xfa_attribute 'id' xfa_attribute 'override' xfa_attribute 'relevant' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'arc', Template::Arc, 0..1 xfa_node 'boolean', Template::Boolean, 0..1 xfa_node 'date', Template::Date, 0..1 xfa_node 'dateTime', Template::DateTime, 0..1 xfa_node 'decimal', Template::Decimal, 0..1 xfa_node 'exData', Template::ExData, 0..1 xfa_node 'float', Template::Float, 0..1 xfa_node 'image', Template::Image, 0..1 xfa_node 'integer', Template::Integer, 0..1 xfa_node 'line', Template::Line, 0..1 xfa_node 'rectangle', Template::Rectangle, 0..1 xfa_node 'text', Template::Text, 0..1 xfa_node 'time', Template::Time, 0..1 def initialize super('value') end end class Caption < XFA::Element xfa_attribute 'id' xfa_attribute 'placement' xfa_attribute 'presence' xfa_attribute 'reserve' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'extras', Template::Extras, 0..1 xfa_node 'font', Template::Font, 0..1 xfa_node 'margin', Template::Margin, 0..1 xfa_node 'para', Template::Para, 0..1 xfa_node 'value', Template::Value, 0..1 def initialize super('caption') end end class Traverse < XFA::Element xfa_attribute 'id' xfa_attribute 'operation' xfa_attribute 'ref' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'extras', Template::Extras, 0..1 xfa_node 'script', Template::Script, 0..1 def initialize super('traverse') end end class Traversal < XFA::Element xfa_attribute 'id' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'extras', Template::Extras, 0..1 xfa_node 'traverse', Template::Traverse def initialize super('traversal') end end class Certificate < XFA::Element xfa_attribute 'id' xfa_attribute 'name' xfa_attribute 'use' xfa_attribute 'usehref' def initialize(b64data = nil) super('certificate') self.text = b64data end end class Encrypt < XFA::Element xfa_attribute 'id' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'certificate', Template::Certificate, 0..1 def initialize super('encrypt') end end class Barcode < XFA::Element xfa_attribute 'charEncoding' xfa_attribute 'checksum' xfa_attribute 'dataColumnCount' xfa_attribute 'dataLength' xfa_attribute 'dataPrep' xfa_attribute 'dataRowCount' xfa_attribute 'endChar' xfa_attribute 'errorConnectionLevel' xfa_attribute 'id' xfa_attribute 'moduleHeight' xfa_attribute 'moduleWidth' xfa_attribute 'printCheckDigit' xfa_attribute 'rowColumnRatio' xfa_attribute 'startChar' xfa_attribute 'textLocation' xfa_attribute 'truncate' xfa_attribute 'type' xfa_attribute 'upsMode' xfa_attribute 'use' xfa_attribute 'usehref' xfa_attribute 'wideNarrowRatio' xfa_node 'encrypt', Template::Encrypt, 0..1 xfa_node 'extras', Template::Extras, 0..1 def initialize super('barcode') end end class Button < XFA::Element xfa_attribute 'highlight' xfa_attribute 'id' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'extras', Template::Extras, 0..1 def initialize super('button') end end class CheckButton < XFA::Element xfa_attribute 'id' xfa_attribute 'mark' xfa_attribute 'shape' xfa_attribute 'size' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'border', Template::Border, 0..1 xfa_node 'extras', Template::Extras, 0..1 xfa_node 'margin', Template::Margin, 0..1 def initialize super('checkButton') end end class ChoiceList < XFA::Element xfa_attribute 'commitOn' xfa_attribute 'id' xfa_attribute 'open' xfa_attribute 'textEntry' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'border', Template::Border, 0..1 xfa_node 'extras', Template::Extras, 0..1 xfa_node 'margin', Template::Margin, 0..1 def initialize super('choiceList') end end class Comb < XFA::Element xfa_attribute 'id' xfa_attribute 'numberOfCells' xfa_attribute 'use' xfa_attribute 'usehref' def initialize super('comb') end end class DateTimeEdit < XFA::Element xfa_attribute 'hScrollPolicy' xfa_attribute 'id' xfa_attribute 'picker' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'border', Template::Border, 0..1 xfa_node 'comb', Template::Comb, 0..1 xfa_node 'extras', Template::Extras, 0..1 xfa_node 'margin', Template::Margin, 0..1 def initialize super('dateTimeEdit') end end class DefaultUI < XFA::Element xfa_attribute 'id' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'extras', Template::Extras, 0..1 def initialize super('defaultUi') end end class ImageEdit < XFA::Element xfa_attribute 'data' xfa_attribute 'id' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'border', Template::Border, 0..1 xfa_node 'extras', Template::Extras, 0..1 xfa_node 'margin', Template::Margin, 0..1 def initialize super('imageEdit') end end class NumericEdit < XFA::Element xfa_attribute 'hScrollPolicy' xfa_attribute 'id' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'border', Template::Border, 0..1 xfa_node 'comb', Template::Comb, 0..1 xfa_node 'extras', Template::Extras, 0..1 xfa_node 'margin', Template::Margin, 0..1 def initialize super('numericEdit') end end class PasswordEdit < XFA::Element xfa_attribute 'hScrollPolicy' xfa_attribute 'id' xfa_attribute 'passwordChar' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'border', Template::Border, 0..1 xfa_node 'extras', Template::Extras, 0..1 xfa_node 'margin', Template::Margin, 0..1 def initialize super('passwordEdit') end end class AppearanceFilter < XFA::Element xfa_attribute 'id' xfa_attribute 'type' xfa_attribute 'use' xfa_attribute 'usehref' def initialize(name = "") super('appearanceFilter') self.text = name end end class Issuers < XFA::Element xfa_attribute 'id' xfa_attribute 'type' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'certificate', Template::Certificate def initialize super('issuers') end end class KeyUsage < XFA::Element xfa_attribute 'crlSign' xfa_attribute 'dataEncipherment' xfa_attribute 'decipherOnly' xfa_attribute 'digitalSignature' xfa_attribute 'encipherOnly' xfa_attribute 'id' xfa_attribute 'keyAgreement' xfa_attribute 'keyCertSign' xfa_attribute 'keyEncipherment' xfa_attribute 'nonRepudiation' xfa_attribute 'type' xfa_attribute 'use' xfa_attribute 'usehref' def initialize super('keyUsage') end end class OID < XFA::Element xfa_attribute 'id' xfa_attribute 'name' xfa_attribute 'use' xfa_attribute 'usehref' def initialize(oid = "") super('oid') self.text = oid end end class OIDs < XFA::Element xfa_attribute 'id' xfa_attribute 'type' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'oid', Template::OID def initialize super('oids') end end class Signing < XFA::Element xfa_attribute 'id' xfa_attribute 'type' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'certificate', Template::Certificate def initialize super('signing') end end class SubjectDN < XFA::Element xfa_attribute 'delimiter' xfa_attribute 'id' xfa_attribute 'name' xfa_attribute 'use' xfa_attribute 'usehref' def initialize(data = "") super('subjectDN') self.text = data end end class SubjectDNs < XFA::Element xfa_attribute 'id' xfa_attribute 'type' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'subjectDN', Template::SubjectDN, 0..1 def initialize super('subjectDNs') end end class Certificates < XFA::Element xfa_attribute 'credentialServerPolicy' xfa_attribute 'id' xfa_attribute 'url' xfa_attribute 'urlPolicy' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'issuers', Template::Issuers, 0..1 xfa_node 'keyUsage', Template::KeyUsage, 0..1 xfa_node 'oids', Template::OIDs, 0..1 xfa_node 'signing', Template::Signing, 0..1 xfa_node 'subjectDNs', Template::SubjectDNs, 0..1 def initialize super('certificates') end end class DigestMethod < XFA::Element xfa_attribute 'id' xfa_attribute 'use' xfa_attribute 'usehref' def initialize(method = "") super('digestMethod') self.text = method end end class DigestMethods < XFA::Element xfa_attribute 'id' xfa_attribute 'type' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'digestMethod', Template::DigestMethod def initialize super('digestMethods') end end class Encoding < XFA::Element xfa_attribute 'id' xfa_attribute 'use' xfa_attribute 'usehref' def initialize(encoding = "") super('encoding') self.text = encoding end end class Encodings < XFA::Element xfa_attribute 'id' xfa_attribute 'type' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'encoding', Template::Encoding def initialize super('encodings') end end class Handler < XFA::Element xfa_attribute 'id' xfa_attribute 'type' xfa_attribute 'use' xfa_attribute 'usehref' def initialize(handler = "") super('handler') self.text = handler end end class LockDocument < XFA::Element xfa_attribute 'id' xfa_attribute 'type' xfa_attribute 'use' xfa_attribute 'usehref' def initialize(lock = "default") super('lockDocument') self.text = lock end end class MDP < XFA::Element xfa_attribute 'id' xfa_attribute 'permissions' xfa_attribute 'signatureType' xfa_attribute 'use' xfa_attribute 'usehref' def initialize super('mdp') end end class Reason < XFA::Element xfa_attribute 'id' xfa_attribute 'name' xfa_attribute 'use' xfa_attribute 'usehref' def initialize(reason = "") super('reason') self.text = reason end end class Reasons < XFA::Element xfa_attribute 'id' xfa_attribute 'type' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'reason', Template::Reason def initialize super('reasons') end end class TimeStamp < XFA::Element xfa_attribute 'id' xfa_attribute 'server' xfa_attribute 'type' xfa_attribute 'use' xfa_attribute 'usehref' def initialize super('timeStamp') end end class Filter < XFA::Element xfa_attribute 'addRevocationInfo' xfa_attribute 'id' xfa_attribute 'name' xfa_attribute 'use' xfa_attribute 'usehref' xfa_attribute 'version' xfa_node 'appearanceFilter', Template::AppearanceFilter, 0..1 xfa_node 'certificates', Template::Certificates, 0..1 xfa_node 'digestMethods', Template::DigestMethods, 0..1 xfa_node 'encodings', Template::Encodings, 0..1 xfa_node 'handler', Template::Handler, 0..1 xfa_node 'lockDocument', Template::LockDocument, 0..1 xfa_node 'mdp', Template::MDP, 0..1 xfa_node 'reasons', Template::Reasons, 0..1 xfa_node 'timeStamp', Template::TimeStamp, 0..1 def initialize super('filter') end end class Ref < XFA::Element xfa_attribute 'id' xfa_attribute 'use' xfa_attribute 'usehref' def initialize(somexpr = nil) super('ref') self.text = somexpr end end class Manifest < XFA::Element xfa_attribute 'action' xfa_attribute 'id' xfa_attribute 'name' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'extras', Template::Extras, 0..1 xfa_node 'ref', Template::Ref, 0..1 def initialize super('manifest') end end class Signature < XFA::Element xfa_attribute 'id' xfa_attribute 'type' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'border', Template::Border, 0..1 xfa_node 'extras', Template::Extras, 0..1 xfa_node 'filter', Template::Filter, 0..1 xfa_node 'manifest', Template::Manifest, 0..1 xfa_node 'margin', Template::Margin, 0..1 def initialize super('signature') end end class TextEdit < XFA::Element xfa_attribute 'allowRichText' xfa_attribute 'hScrollPolicy' xfa_attribute 'id' xfa_attribute 'multiLine' xfa_attribute 'use' xfa_attribute 'usehref' xfa_attribute 'vScrollPolicy' xfa_node 'border', Template::Border, 0..1 xfa_node 'comb', Template::Comb, 0..1 xfa_node 'extras', Template::Extras, 0..1 xfa_node 'margin', Template::Margin, 0..1 def initialize super('textEdit') end end class UI < XFA::Element xfa_attribute 'id' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'extras', Template::Extras, 0..1 xfa_node 'picture', Template::Picture, 0..1 xfa_node 'barcode', Template::Barcode, 0..1 xfa_node 'button', Template::Button, 0..1 xfa_node 'checkButton', Template::CheckButton, 0..1 xfa_node 'choiceList', Template::ChoiceList, 0..1 xfa_node 'dateTimeEdit', Template::DateTimeEdit, 0..1 xfa_node 'defaultUi', Template::DefaultUI, 0..1 xfa_node 'imageEdit', Template::ImageEdit, 0..1 xfa_node 'numericEdit', Template::NumericEdit, 0..1 xfa_node 'passwordEdit', Template::PasswordEdit, 0..1 xfa_node 'signature', Template::Signature, 0..1 xfa_node 'textEdit', Template::TextEdit, 0..1 def initialize super('ui') end end class SetProperty < XFA::Element xfa_attribute 'connection' xfa_attribute 'ref' xfa_attribute 'target' def initialize super('setProperty') end end class Draw < XFA::Element xfa_attribute 'anchorType' xfa_attribute 'colSpan' xfa_attribute 'h' xfa_attribute 'id' xfa_attribute 'locale' xfa_attribute 'maxH' xfa_attribute 'maxW' xfa_attribute 'minH' xfa_attribute 'minW' xfa_attribute 'name' xfa_attribute 'presence' xfa_attribute 'relevant' xfa_attribute 'rotate' xfa_attribute 'use' xfa_attribute 'usehref' xfa_attribute 'w' xfa_attribute 'x' xfa_attribute 'y' xfa_node 'assist', Template::Assist, 0..1 xfa_node 'border', Template::Border, 0..1 xfa_node 'caption', Template::Caption, 0..1 xfa_node 'desc', Template::Desc, 0..1 xfa_node 'extras', Template::Extras, 0..1 xfa_node 'font', Template::Font, 0..1 xfa_node 'keep', Template::Keep, 0..1 xfa_node 'margin', Template::Margin, 0..1 xfa_node 'para', Template::Para, 0..1 xfa_node 'traversal', Template::Traversal, 0..1 xfa_node 'ui', Template::UI, 0..1 xfa_node 'value', Template::Value, 0..1 xfa_node 'setProperty', Template::SetProperty def initialize super('draw') end end class Validate < XFA::Element xfa_attribute 'formatTest' xfa_attribute 'id' xfa_attribute 'nullTest' xfa_attribute 'scriptTest' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'extras', Template::Extras, 0..1 xfa_node 'message', Template::Message, 0..1 xfa_node 'picture', Template::Picture, 0..1 xfa_node 'script', Template::Script, 0..1 def initialize super('validate') end end class Connect < XFA::Element xfa_attribute 'connection' xfa_attribute 'id' xfa_attribute 'ref' xfa_attribute 'usage' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'picture', Template::Picture, 0..1 def initialize super('connect') end end class Execute < XFA::Element xfa_attribute 'connection' xfa_attribute 'executeType' xfa_attribute 'id' xfa_attribute 'runAt' xfa_attribute 'use' xfa_attribute 'usehref' def initialize super('execute') end end class SignData < XFA::Element xfa_attribute 'id' xfa_attribute 'operation' xfa_attribute 'ref' xfa_attribute 'target' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'filter', Template::Filter, 0..1 xfa_node 'manifest', Template::Manifest, 0..1 def initialize super('signData') end end class Submit < XFA::Element xfa_attribute 'embedPDF' xfa_attribute 'format' xfa_attribute 'id' xfa_attribute 'target' xfa_attribute 'textEncoding' xfa_attribute 'use' xfa_attribute 'usehref' xfa_attribute 'xdpContent' xfa_node 'encrypt', Template::Encrypt, 0..1 xfa_node 'signData', Template::SignData def initialize super('submit') end end class Event < XFA::Element xfa_attribute 'activity' xfa_attribute 'id' xfa_attribute 'listen' xfa_attribute 'name' xfa_attribute 'ref' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'extras', Template::Extras, 0..1 xfa_node 'execute', Template::Execute, 0..1 xfa_node 'script', Template::Script, 0..1 xfa_node 'signData', Template::SignData, 0..1 xfa_node 'submit', Template::Submit, 0..1 def initialize super('event') end end class Format < XFA::Element xfa_attribute 'id' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'extras', Template::Extras, 0..1 xfa_node 'picture', Template::Picture, 0..1 def initialize super('format') end end class Items < XFA::Element xfa_attribute 'id' xfa_attribute 'name' xfa_attribute 'presence' xfa_attribute 'ref' xfa_attribute 'save' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'boolean', Template::Boolean xfa_node 'date', Template::Date xfa_node 'dateTime', Template::DateTime xfa_node 'decimal', Template::Decimal xfa_node 'exData', Template::ExData xfa_node 'float', Template::Float xfa_node 'image', Template::Image xfa_node 'integer', Template::Integer xfa_node 'text', Template::Text xfa_node 'time', Template::Time def initialize super('items') end end class BindItems < XFA::Element xfa_attribute 'connection' xfa_attribute 'labelRef' xfa_attribute 'ref' xfa_attribute 'valueRef' def initialize super('bindItems') end end class Field < XFA::Element xfa_attribute 'access' xfa_attribute 'accessKey' xfa_attribute 'anchorType' xfa_attribute 'colSpan' xfa_attribute 'h' xfa_attribute 'id' xfa_attribute 'locale' xfa_attribute 'maxH' xfa_attribute 'maxW' xfa_attribute 'minH' xfa_attribute 'minW' xfa_attribute 'name' xfa_attribute 'presence' xfa_attribute 'relevant' xfa_attribute 'rotate' xfa_attribute 'use' xfa_attribute 'usehref' xfa_attribute 'w' xfa_attribute 'x' xfa_attribute 'y' xfa_node 'assist', Template::Assist, 0..1 xfa_node 'bind', Template::Bind, 0..1 xfa_node 'border', Template::Border, 0..1 xfa_node 'calculate', Template::Calculate, 0..1 xfa_node 'caption', Template::Caption, 0..1 xfa_node 'desc', Template::Desc, 0..1 xfa_node 'extras', Template::Extras, 0..1 xfa_node 'font', Template::Font, 0..1 xfa_node 'format', Template::Format, 0..1 xfa_node 'items', Template::Items, 0..2 xfa_node 'keep', Template::Keep, 0..1 xfa_node 'margin', Template::Margin, 0..1 xfa_node 'para', Template::Para, 0..1 xfa_node 'traversal', Template::Traversal, 0..1 xfa_node 'ui', Template::UI, 0..1 xfa_node 'validate', Template::Validate, 0..1 xfa_node 'value', Template::Value, 0..1 xfa_node 'bindItems', Template::BindItems xfa_node 'connect', Template::Connect xfa_node 'event', Template::Event xfa_node 'setProperty', Template::SetProperty def initialize super('field') end end class ExclGroup < XFA::Element xfa_attribute 'access' xfa_attribute 'accessKey' xfa_attribute 'anchorType' xfa_attribute 'colSpan' xfa_attribute 'h' xfa_attribute 'id' xfa_attribute 'layout' xfa_attribute 'maxH' xfa_attribute 'maxW' xfa_attribute 'minH' xfa_attribute 'minW' xfa_attribute 'name' xfa_attribute 'presence' xfa_attribute 'relevant' xfa_attribute 'use' xfa_attribute 'usehref' xfa_attribute 'w' xfa_attribute 'x' xfa_attribute 'y' xfa_node 'assist', Template::Assist, 0..1 xfa_node 'bind', Template::Bind, 0..1 xfa_node 'border', Template::Border, 0..1 xfa_node 'calculate', Template::Calculate, 0..1 xfa_node 'caption', Template::Caption, 0..1 xfa_node 'desc', Template::Desc, 0..1 xfa_node 'extras', Template::Extras, 0..1 xfa_node 'margin', Template::Margin, 0..1 xfa_node 'para', Template::Para, 0..1 xfa_node 'traversal', Template::Traversal, 0..1 xfa_node 'validate', Template::Validate, 0..1 xfa_node 'connect', Template::Connect xfa_node 'event', Template::Event xfa_node 'field', Template::Field xfa_node 'setProperty', Template::SetProperty def initialize super('exclGroup') end end class BreakAfter < XFA::Element xfa_attribute 'id' xfa_attribute 'leader' xfa_attribute 'startNew' xfa_attribute 'target' xfa_attribute 'targetType' xfa_attribute 'trailer' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'script', Template::Script, 0..1 def initialize super('breakAfter') end end class BreakBefore < XFA::Element xfa_attribute 'id' xfa_attribute 'leader' xfa_attribute 'startNew' xfa_attribute 'target' xfa_attribute 'targetType' xfa_attribute 'trailer' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'script', Template::Script, 0..1 def initialize super('breakBefore') end end class Subform < XFA::Element ; end class SubformSet < XFA::Element xfa_attribute 'id' xfa_attribute 'name' xfa_attribute 'relation' xfa_attribute 'relevant' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'bookend', Template::Bookend, 0..1 xfa_node 'break', Template::Break, 0..1 xfa_node 'desc', Template::Desc, 0..1 xfa_node 'extras', Template::Extras, 0..1 xfa_node 'occur', Template::Occur, 0..1 xfa_node 'overflow', Template::Overflow, 0..1 xfa_node 'breakAfter', Template::BreakAfter xfa_node 'breakBefore', Template::BreakBefore xfa_node 'subform', Template::Subform xfa_node 'subformSet', Template::SubformSet def initialize super('subformSet') end end class Area < XFA::Element xfa_attribute 'colSpan' xfa_attribute 'id' xfa_attribute 'name' xfa_attribute 'relevant' xfa_attribute 'use' xfa_attribute 'usehref' xfa_attribute 'x' xfa_attribute 'y' xfa_node 'desc', Template::Desc, 0..1 xfa_node 'extras', Template::Extras, 0..1 xfa_node 'area', Template::Area xfa_node 'draw', Template::Draw xfa_node 'exclGroup', Template::ExclGroup xfa_node 'field', Template::Field xfa_node 'subform', Template::Subform xfa_node 'subformSet', Template::SubformSet def initialize super('area') end end class ContentArea < XFA::Element xfa_attribute 'h' xfa_attribute 'id' xfa_attribute 'name' xfa_attribute 'relevant' xfa_attribute 'use' xfa_attribute 'usehref' xfa_attribute 'w' xfa_attribute 'x' xfa_attribute 'y' xfa_node 'desc', Template::Desc, 0..1 xfa_node 'extras', Template::Extras, 0..1 def initialize super('contentArea') end end class PageArea < XFA::Element xfa_attribute 'blankOrNotBlank' xfa_attribute 'id' xfa_attribute 'initialNumber' xfa_attribute 'name' xfa_attribute 'numbered' xfa_attribute 'oddOrEven' xfa_attribute 'pagePosition' xfa_attribute 'relevant' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'desc', Template::Desc, 0..1 xfa_node 'extras', Template::Extras, 0..1 xfa_node 'medium', Template::Medium, 0..1 xfa_node 'occur', Template::Occur, 0..1 xfa_node 'area', Template::Area xfa_node 'contentArea', Template::ContentArea xfa_node 'draw', Template::Draw xfa_node 'exclGroup', Template::ExclGroup xfa_node 'field', Template::Field xfa_node 'subform', Template::Subform def initialize super('pageArea') end end class PageSet < XFA::Element xfa_attribute 'id' xfa_attribute 'name' xfa_attribute 'relation' xfa_attribute 'relevant' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'extras', Template::Extras, 0..1 xfa_node 'occur', Template::Occur, 0..1 xfa_node 'pageArea', Template::PageArea xfa_node 'pageSet', Template::PageSet ORDERED_OCCURENCE = "orderedOccurence" DUPLEX_PAGINATED = "duplexPaginated" SIMPLEX_PAGINATED = "simplexPaginated" def initialize super('pageSet') end end class Variables < XFA::Element xfa_attribute 'id' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'boolean', Template::Boolean xfa_node 'date', Template::Date xfa_node 'dateTime', Template::DateTime xfa_node 'decimal', Template::Decimal xfa_node 'exData', Template::ExData xfa_node 'float', Template::Float xfa_node 'image', Template::Image xfa_node 'integer', Template::Integer xfa_node 'manifest', Template::Manifest xfa_node 'script', Template::Script xfa_node 'text', Template::Text xfa_node 'time', Template::Time def initialize super('variables') end end class ExObject < XFA::Element xfa_attribute 'archive' xfa_attribute 'classId' xfa_attribute 'codeBase' xfa_attribute 'codeType' xfa_attribute 'id' xfa_attribute 'name' xfa_attribute 'use' xfa_attribute 'usehref' xfa_node 'extras', Template::Extras, 0..1 xfa_node 'boolean', Template::Boolean xfa_node 'date', Template::Date xfa_node 'dateTime', Template::DateTime xfa_node 'decimal', Template::Decimal xfa_node 'exData', Template::ExData xfa_node 'exObject', Template::ExObject xfa_node 'float', Template::Float xfa_node 'image', Template::Image xfa_node 'integer', Template::Integer xfa_node 'text', Template::Text xfa_node 'time', Template::Time def initialize super('exObject') end end class Proto < XFA::Element xfa_node 'appearanceFilter', Template::AppearanceFilter xfa_node 'arc', Template::Arc xfa_node 'area', Template::Area xfa_node 'assist', Template::Assist xfa_node 'barcode', Template::Barcode xfa_node 'bindItems', Template::BindItems xfa_node 'bookend', Template::Bookend xfa_node 'boolean', Template::Boolean xfa_node 'border', Template::Border xfa_node 'break', Template::Break xfa_node 'breakAfter', Template::BreakAfter xfa_node 'breakBefore', Template::BreakBefore xfa_node 'button', Template::Button xfa_node 'calculate', Template::Calculate xfa_node 'caption', Template::Caption xfa_node 'certificate', Template::Certificate xfa_node 'certificates', Template::Certificates xfa_node 'checkButton', Template::CheckButton xfa_node 'choiceList', Template::ChoiceList xfa_node 'color', Template::Color xfa_node 'comb', Template::Comb xfa_node 'connect', Template::Connect xfa_node 'contentArea', Template::ContentArea xfa_node 'corner', Template::Corner xfa_node 'date', Template::Date xfa_node 'dateTime', Template::DateTime xfa_node 'dateTimeEdit', Template::DateTimeEdit xfa_node 'decimal', Template::Decimal xfa_node 'defaultUi', Template::DefaultUI xfa_node 'desc', Template::Desc xfa_node 'digestMethod', Template::DigestMethod xfa_node 'digestMethods', Template::DigestMethods xfa_node 'draw', Template::Draw xfa_node 'edge', Template::Edge xfa_node 'encoding', Template::Encoding xfa_node 'encodings', Template::Encodings xfa_node 'encrypt', Template::Encrypt xfa_node 'event', Template::Event xfa_node 'exData', Template::ExData xfa_node 'exObject', Template::ExObject xfa_node 'exclGroup', Template::ExclGroup xfa_node 'execute', Template::Execute xfa_node 'extras', Template::Extras xfa_node 'field', Template::Field xfa_node 'fill', Template::Fill xfa_node 'filter', Template::Filter xfa_node 'float', Template::Float xfa_node 'font', Template::Font xfa_node 'format', Template::Format xfa_node 'handler', Template::Handler xfa_node 'hyphenation', Template::Hyphenation xfa_node 'image', Template::Image xfa_node 'imageEdit', Template::ImageEdit xfa_node 'integer', Template::Integer xfa_node 'issuers', Template::Issuers xfa_node 'items', Template::Items xfa_node 'keep', Template::Keep xfa_node 'keyUsage', Template::KeyUsage xfa_node 'line', Template::Line xfa_node 'linear', Template::Linear xfa_node 'lockDocument', Template::LockDocument xfa_node 'manifest', Template::Manifest xfa_node 'margin', Template::Margin xfa_node 'mdp', Template::MDP xfa_node 'medium', Template::Medium xfa_node 'message', Template::Message xfa_node 'numericEdit', Template::NumericEdit xfa_node 'occur', Template::Occur xfa_node 'oid', Template::OID xfa_node 'oids', Template::OIDs xfa_node 'overflow', Template::Overflow xfa_node 'pageArea', Template::PageArea xfa_node 'pageSet', Template::PageSet xfa_node 'para', Template::Para xfa_node 'passwordEdit', Template::PasswordEdit xfa_node 'pattern', Template::Pattern xfa_node 'picture', Template::Picture xfa_node 'radial', Template::Radial xfa_node 'reason', Template::Reason xfa_node 'reasons', Template::Reasons xfa_node 'rectangle', Template::Rectangle xfa_node 'ref', Template::Ref xfa_node 'script', Template::Script xfa_node 'setProperty', Template::SetProperty xfa_node 'signData', Template::SignData xfa_node 'signature', Template::Signature xfa_node 'signing', Template::Signing xfa_node 'solid', Template::Solid xfa_node 'speak', Template::Speak xfa_node 'stipple', Template::Stipple xfa_node 'subform', Template::Subform xfa_node 'subformSet', Template::SubformSet xfa_node 'subjectDN', Template::SubjectDN xfa_node 'subjectDNs', Template::SubjectDNs xfa_node 'submit', Template::Submit xfa_node 'text', Template::Text xfa_node 'textEdit', Template::TextEdit xfa_node 'time', Template::Time xfa_node 'timeStamp', Template::TimeStamp xfa_node 'toolTip', Template::ToolTip xfa_node 'traversal', Template::Traversal xfa_node 'traverse', Template::Traverse xfa_node 'ui', Template::UI xfa_node 'validate', Template::Validate xfa_node 'value', Template::Value xfa_node 'variables', Template::Variables def initialize super('proto') end end class Subform < XFA::Element xfa_attribute 'access' xfa_attribute 'allowMacro' xfa_attribute 'anchorType' xfa_attribute 'colSpan' xfa_attribute 'columnWidths' xfa_attribute 'h' xfa_attribute 'id' xfa_attribute 'layout' xfa_attribute 'locale' xfa_attribute 'maxH' xfa_attribute 'maxW' xfa_attribute 'minH' xfa_attribute 'minW' xfa_attribute 'name' xfa_attribute 'presence' xfa_attribute 'relevant' xfa_attribute 'restoreState' xfa_attribute 'scope' xfa_attribute 'use' xfa_attribute 'usehref' xfa_attribute 'w' xfa_attribute 'x' xfa_attribute 'y' xfa_node 'assist', Template::Assist, 0..1 xfa_node 'bind', Template::Bind, 0..1 xfa_node 'bookend', Template::Bookend, 0..1 xfa_node 'border', Template::Border, 0..1 xfa_node 'break', Template::Break, 0..1 xfa_node 'calculate', Template::Calculate, 0..1 xfa_node 'desc', Template::Desc, 0..1 xfa_node 'extras', Template::Extras, 0..1 xfa_node 'keep', Template::Keep, 0..1 xfa_node 'margin', Template::Margin, 0..1 xfa_node 'occur', Template::Occur, 0..1 xfa_node 'overflow', Template::Overflow, 0..1 xfa_node 'pageSet', Template::PageSet, 0..1 xfa_node 'para', Template::Para, 0..1 xfa_node 'traversal', Template::Traversal, 0..1 xfa_node 'validate', Template::Validate, 0..1 xfa_node 'variables', Template::Variables, 0..1 xfa_node 'area', Template::Area xfa_node 'breakAfter', Template::BreakAfter xfa_node 'breakBefore', Template::BreakBefore xfa_node 'connect', Template::Connect xfa_node 'draw', Template::Draw xfa_node 'event', Template::Event xfa_node 'exObject', Template::ExObject xfa_node 'exclGroup', Template::ExclGroup xfa_node 'field', Template::Field xfa_node 'proto', Template::Proto xfa_node 'setProperty', Template::SetProperty xfa_node 'subform', Template::Subform xfa_node 'subformSet', Template::SubformSet def initialize super('subform') end end xfa_attribute 'baseProfile' xfa_node 'extras', Template::Extras, 0..1 xfa_node 'subform', Template::Subform def initialize super("template") add_attribute 'xmlns:xfa', 'http://www.xfa.org/schema/xfa-template/3.0/' end end # # The _xdc_ packet encloses application-specific XFA driver configuration instruction. # class XDC < XFA::Element mime_type '' def initialize super("xsl:xdc") add_attribute 'xmlns:xdc', 'http://www.xfa.org/schema/xdc/1.0/' end end # # The _xfdf_ (annotations) packet enclosed collaboration annotations placed upon a PDF document. # class XFDF < XFA::Element mime_type 'application/vnd.adobe.xfdf' def initialize super("xfdf") add_attribute 'xmlns', 'http://ns.adobe.com/xfdf/' add_attribute 'xml:space', 'preserve' end end # # An _XMP_ packet contains XML representation of PDF metadata. # class XMPMeta < XFA::Element mime_type 'application/rdf+xml' def initialize super("xmpmeta") add_attribute 'xmlns', 'http://ns.adobe.com/xmpmeta/' add_attribute 'xml:space', 'preserve' end end end class XDP < XFA::Element xfa_attribute 'uuid' xfa_attribute 'timeStamp' xfa_node 'config', Origami::XDP::Packet::Config, 0..1 xfa_node 'connectionSet', Origami::XDP::Packet::ConnectionSet, 0..1 xfa_node 'datasets', Origami::XDP::Packet::Datasets, 0..1 xfa_node 'localeSet', Origami::XDP::Packet::LocaleSet, 0..1 xfa_node 'pdf', Origami::XDP::Packet::PDF, 0..1 xfa_node 'sourceSet', Origami::XDP::Packet::SourceSet, 0..1 xfa_node 'styleSheet', Origami::XDP::Packet::StyleSheet, 0..1 xfa_node 'template', Origami::XDP::Packet::Template, 0..1 xfa_node 'xdc', Origami::XDP::Packet::XDC, 0..1 xfa_node 'xfdf', Origami::XDP::Packet::XFDF, 0..1 xfa_node 'xmpmeta', Origami::XDP::Packet::XMPMeta, 0..1 def initialize super('xdp:xdp') add_attribute 'xmlns:xdp', 'http://ns.adobe.com/xdp/' end end class Package < REXML::Document def initialize(package = nil) super(package || REXML::XMLDecl.new.to_s) add_element Origami::XDP::XDP.new if package.nil? end end end end origami-2.0.0/lib/origami/outline.rb0000644000004100000410000000410112757133666017405 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end module Origami class OutlineItem < Dictionary include StandardObject module Style ITALIC = 1 << 0 BOLD = 1 << 1 end field :Title, :Type => String, :Required => true field :Parent, :Type => Dictionary, :Required => true field :Prev, :Type => OutlineItem field :Next, :Type => OutlineItem field :First, :Type => OutlineItem field :Last, :Type => OutlineItem field :Count, :Type => Integer field :Dest, :Type => [ Name, String, Destination ] field :A, :Type => Action, :Version => "1.1" field :SE, :Type => Dictionary, :Version => "1.3" field :C, :Type => Array.of(Number, length: 3), :Default => [ 0.0, 0.0, 0.0 ], :Version => "1.4" field :F, :Type => Integer, :Default => 0, :Version => "1.4" end class Outline < Dictionary include StandardObject field :Type, :Type => Name, :Default => :Outlines field :First, :Type => OutlineItem field :Last, :Type => OutlineItem field :Count, :Type => Integer end end origami-2.0.0/lib/origami/string.rb0000644000004100000410000004002012757133666017234 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end require 'date' module Origami # # Module common to String objects. # module String module Encoding class EncodingError < Error #:nodoc: end module PDFDocEncoding CHARMAP = [ "\x00\x00", "\xff\xfd", "\xff\xfd", "\xff\xfd", "\xff\xfd", "\xff\xfd", "\xff\xfd", "\xff\xfd", "\xff\xfd", "\x00\x09", "\x00\x0a", "\xff\xfd", "\x00\x0c", "\x00\x0d", "\xff\xfd", "\xff\xfd", "\xff\xfd", "\xff\xfd", "\xff\xfd", "\xff\xfd", "\xff\xfd", "\xff\xfd", "\xff\xfd", "\xff\xfd", "\x02\xd8", "\x02\xc7", "\x02\xc6", "\x02\xd9", "\x02\xdd", "\x02\xdb", "\x02\xda", "\x02\xdc", "\x00\x20", "\x00\x21", "\x00\x22", "\x00\x23", "\x00\x24", "\x00\x25", "\x00\x26", "\x00\x27", "\x00\x28", "\x00\x29", "\x00\x2a", "\x00\x2b", "\x00\x2c", "\x00\x2d", "\x00\x2e", "\x00\x2f", "\x00\x30", "\x00\x31", "\x00\x32", "\x00\x33", "\x00\x34", "\x00\x35", "\x00\x36", "\x00\x37", "\x00\x38", "\x00\x39", "\x00\x3a", "\x00\x3b", "\x00\x3c", "\x00\x3d", "\x00\x3e", "\x00\x3f", "\x00\x40", "\x00\x41", "\x00\x42", "\x00\x43", "\x00\x44", "\x00\x45", "\x00\x46", "\x00\x47", "\x00\x48", "\x00\x49", "\x00\x4a", "\x00\x4b", "\x00\x4c", "\x00\x4d", "\x00\x4e", "\x00\x4f", "\x00\x50", "\x00\x51", "\x00\x52", "\x00\x53", "\x00\x54", "\x00\x55", "\x00\x56", "\x00\x57", "\x00\x58", "\x00\x59", "\x00\x5a", "\x00\x5b", "\x00\x5c", "\x00\x5d", "\x00\x5e", "\x00\x5f", "\x00\x60", "\x00\x61", "\x00\x62", "\x00\x63", "\x00\x64", "\x00\x65", "\x00\x66", "\x00\x67", "\x00\x68", "\x00\x69", "\x00\x6a", "\x00\x6b", "\x00\x6c", "\x00\x6d", "\x00\x6e", "\x00\x6f", "\x00\x70", "\x00\x71", "\x00\x72", "\x00\x73", "\x00\x74", "\x00\x75", "\x00\x76", "\x00\x77", "\x00\x78", "\x00\x79", "\x00\x7a", "\x00\x7b", "\x00\x7c", "\x00\x7d", "\x00\x7e", "\xff\xfd", "\x20\x22", "\x20\x20", "\x20\x21", "\x20\x26", "\x20\x14", "\x20\x13", "\x01\x92", "\x20\x44", "\x20\x39", "\x20\x3a", "\x22\x12", "\x20\x30", "\x20\x1e", "\x20\x1c", "\x20\x1d", "\x20\x18", "\x20\x19", "\x20\x1a", "\x21\x22", "\xfb\x01", "\xfb\x02", "\x01\x41", "\x01\x52", "\x01\x60", "\x01\x78", "\x01\x7d", "\x01\x31", "\x01\x42", "\x01\x53", "\x01\x61", "\x01\x7e", "\xff\xfd", "\x20\xac", "\x00\xa1", "\x00\xa2", "\x00\xa3", "\x00\xa4", "\x00\xa5", "\x00\xa6", "\x00\xa7", "\x00\xa8", "\x00\xa9", "\x00\xaa", "\x00\xab", "\x00\xac", "\xff\xfd", "\x00\xae", "\x00\xaf", "\x00\xb0", "\x00\xb1", "\x00\xb2", "\x00\xb3", "\x00\xb4", "\x00\xb5", "\x00\xb6", "\x00\xb7", "\x00\xb8", "\x00\xb9", "\x00\xba", "\x00\xbb", "\x00\xbc", "\x00\xbd", "\x00\xbe", "\x00\xbf", "\x00\xc0", "\x00\xc1", "\x00\xc2", "\x00\xc3", "\x00\xc4", "\x00\xc5", "\x00\xc6", "\x00\xc7", "\x00\xc8", "\x00\xc9", "\x00\xca", "\x00\xcb", "\x00\xcc", "\x00\xcd", "\x00\xce", "\x00\xcf", "\x00\xd0", "\x00\xd1", "\x00\xd2", "\x00\xd3", "\x00\xd4", "\x00\xd5", "\x00\xd6", "\x00\xd7", "\x00\xd8", "\x00\xd9", "\x00\xda", "\x00\xdb", "\x00\xdc", "\x00\xdd", "\x00\xde", "\x00\xdf", "\x00\xe0", "\x00\xe1", "\x00\xe2", "\x00\xe3", "\x00\xe4", "\x00\xe5", "\x00\xe6", "\x00\xe7", "\x00\xe8", "\x00\xe9", "\x00\xea", "\x00\xeb", "\x00\xec", "\x00\xed", "\x00\xee", "\x00\xef", "\x00\xf0", "\x00\xf1", "\x00\xf2", "\x00\xf3", "\x00\xf4", "\x00\xf5", "\x00\xf6", "\x00\xf7", "\x00\xf8", "\x00\xf9", "\x00\xfa", "\x00\xfb", "\x00\xfc", "\x00\xfd", "\x00\xfe", "\x00\xff" ].map(&:b) def PDFDocEncoding.to_utf16be(pdfdocstr) utf16bestr = UTF16BE::BOM.dup pdfdocstr.each_byte do |byte| utf16bestr << CHARMAP[byte] end utf16bestr.force_encoding('binary') end def PDFDocEncoding.to_pdfdoc(str) str end end module UTF16BE BOM = "\xFE\xFF".b def UTF16BE.to_utf16be(str) str end def UTF16BE.to_pdfdoc(str) pdfdoc = [] i = 2 while i < str.size char = PDFDocEncoding::CHARMAP.index(str[i,2]) raise EncodingError, "Can't convert UTF16-BE character to PDFDocEncoding" if char.nil? pdfdoc << char i = i + 2 end pdfdoc.pack("C*") end end end module ClassMethods #:nodoc:all def native_type; Origami::String end end def self.included(receiver) #:nodoc: receiver.extend(ClassMethods) end def self.native_type; Origami::String end #:nodoc: include Origami::Object attr_accessor :encoding def initialize(str) #:nodoc: super(str.force_encoding('binary')) detect_encoding end # # Convert String object to an UTF8 encoded Ruby string. # def to_utf8 detect_encoding utf16 = self.encoding.to_utf16be(self.value) utf16.slice!(0, Encoding::UTF16BE::BOM.size) utf16.encode("utf-8", "utf-16be") end # # Convert String object to an UTF16-BE encoded binary Ruby string. # def to_utf16be detect_encoding self.encoding.to_utf16be(self.value) end # # Convert String object to a PDFDocEncoding encoded binary Ruby string. # def to_pdfdoc detect_encoding self.encoding.to_pdfdoc(self.value) end def detect_encoding #:nodoc: if self.value[0,2] == Encoding::UTF16BE::BOM @encoding = Encoding::UTF16BE else @encoding = Encoding::PDFDocEncoding end end end class InvalidHexaStringObjectError < InvalidObjectError #:nodoc: end # # Class representing an hexadecimal-writen String Object. # class HexaString < ::String include String TOKENS = %w{ < > } #:nodoc: @@regexp_open = Regexp.new(WHITESPACES + TOKENS.first) @@regexp_close = Regexp.new(TOKENS.last) # # Creates a new PDF hexadecimal String. # _str_:: The string value. # def initialize(str = "") unless str.is_a?(::String) raise TypeError, "Expected type String, received #{str.class}." end super(str) end def self.parse(stream, parser = nil) #:nodoc: offset = stream.pos if stream.skip(@@regexp_open).nil? raise InvalidHexaStringObjectError, "Hexadecimal string shall start with a '#{TOKENS.first}' token" end hexa = stream.scan_until(@@regexp_close) if hexa.nil? raise InvalidHexaStringObjectError, "Hexadecimal string shall end with a '#{TOKENS.last}' token" end decoded = Filter::ASCIIHex.decode(hexa.chomp!(TOKENS.last)) hexastr = HexaString.new(decoded) hexastr.file_offset = offset hexastr end def to_s #:nodoc: super(TOKENS.first + Filter::ASCIIHex.encode(to_str) + TOKENS.last) end # # Converts self to a literal String. # def to_literal LiteralString.new(self.value) end def value self.decrypt! if self.is_a?(Encryption::EncryptedString) and not @decrypted to_str end end class InvalidLiteralStringObjectError < InvalidObjectError #:nodoc: end # # Class representing a literal String Object. # class LiteralString < ::String include String TOKENS = %w{ ( ) } #:nodoc: @@regexp_open = Regexp.new(WHITESPACES + Regexp.escape(TOKENS.first)) @@regexp_close = Regexp.new(Regexp.escape(TOKENS.last)) # # Creates a new PDF String. # _str_:: The string value. # def initialize(str = "") unless str.is_a?(::String) raise TypeError, "Expected type String, received #{str.class}." end super(str) end def self.parse(stream, parser = nil) #:nodoc: offset = stream.pos unless stream.skip(@@regexp_open) raise InvalidLiteralStringObjectError, "No literal string start token found" end result = "" depth = 0 while depth != 0 or stream.peek(1) != TOKENS.last do raise InvalidLiteralStringObjectError, "Non-terminated string" if stream.eos? c = stream.get_byte case c when "\\" if stream.match?(/\d{1,3}/) oct = stream.peek(3).oct.chr stream.pos += 3 result << oct elsif stream.match?(/((\r?\n)|(\r\n?))/) stream.skip(/((\r?\n)|(\r\n?))/) next else flag = stream.get_byte case flag when "n" then result << "\n" when "r" then result << "\r" when "t" then result << "\t" when "b" then result << "\b" when "f" then result << "\f" when "(" then result << "(" when ")" then result << ")" when "\\" then result << "\\" when "\r" stream.pos += 1 if stream.peek(1) == "\n" when "\n" else result << flag end end when "(" then depth = depth + 1 result << c when ")" then depth = depth - 1 result << c else result << c end end unless stream.skip(@@regexp_close) raise InvalidLiteralStringObjectError, "Byte string shall be terminated with '#{TOKENS.last}'" end # Try to cast as a Date object if possible. if result[0, 2] == 'D:' begin date = Date.parse(result) date.file_offset = offset return date rescue InvalidDateError end end bytestr = self.new(result) bytestr.file_offset = offset bytestr end def expand #:nodoc: extended = self.gsub("\\", "\\\\\\\\") extended.gsub!(/\)/, "\\)") extended.gsub!("\n", "\\n") extended.gsub!("\r", "\\r") extended.gsub!(/\(/, "\\(") extended end def to_s #:nodoc: super(TOKENS.first + self.expand + TOKENS.last) end # # Converts self to HexaString # def to_hex HexaString.new(self.value) end # # Returns a standard String representation. # def value self.decrypt! if self.is_a?(Encryption::EncryptedString) and not @decrypted to_str end end class InvalidDateError < Error #:nodoc: end # # Class representing a Date string. # class Date < LiteralString #:nodoc: REGEXP_TOKEN = /D: # Date header (?\d{4}) # Year (?\d{2})? # Month (?\d{2})? # Day (?\d{2})? # Hour (?\d{2})? # Minute (?\d{2})? # Second (?: (?[\+\-Z]) # UT relationship (?\d{2}) # UT hour offset ('(?\d{2}))? # UT minute offset )? /x attr_reader :year, :month, :day, :hour, :min, :sec, :utc_offset def initialize(year:, month: 1, day: 1, hour: 0, min: 0, sec: 0, utc_offset: 0) raise InvalidDateError, "Invalid year #{year}" unless (0..9999) === year raise InvalidDateError, "Invalid month #{month}" unless (1..12) === month raise InvalidDateError, "Invalid day #{day}" unless (1..31) === day raise InvalidDateError, "Invalid hour #{hour}" unless (0..23) === hour raise InvalidDateError, "Invalid minute #{min}" unless (0..59) === min raise InvalidDateError, "Invalid second #{sec}" unless (0..59) === sec @year, @month, @day, @hour, @min, @sec = year, month, day, hour, min, sec @utc_offset = utc_offset date = "D:%04d%02d%02d%02d%02d%02d" % [year, month, day, hour, min, sec ] if utc_offset == 0 date << "Z00'00" else date << (if utc_offset < 0 then '-' else '+' end) off_hours, off_secs = utc_offset.abs.divmod(3600) off_mins = off_secs / 60 date << "%02d'%02d" % [ off_hours, off_mins ] end super(date) end def to_datetime ::DateTime.new(@year, @month, @day, @hour, @min, @sec, (@utc_offset / 3600).to_s) end def self.parse(str) #:nodoc: raise InvalidDateError, "Not a valid Date string" unless str =~ REGEXP_TOKEN date = { year: $~['year'].to_i } date[:month] = $~['month'].to_i if $~['month'] date[:day] = $~['day'].to_i if $~['day'] date[:hour] = $~['hour'].to_i if $~['hour'] date[:min] = $~['min'].to_i if $~['min'] date[:sec] = $~['sec'].to_i if $~['sec'] if %w[+ -].include?($~['ut']) utc_offset = $~['ut_hour_off'].to_i * 3600 + $~['ut_min_off'].to_i * 60 utc_offset = -utc_offset if $~['ut'] == '-' date[:utc_offset] = utc_offset end Origami::Date.new(date) end # # Returns current Date String in UTC time. # def self.now now = Time.now.utc date = { year: now.strftime("%Y").to_i, month: now.strftime("%m").to_i, day: now.strftime("%d").to_i, hour: now.strftime("%H").to_i, min: now.strftime("%M").to_i, sec: now.strftime("%S").to_i, utc_offset: now.utc_offset } Origami::Date.new(date) end end end origami-2.0.0/lib/origami/pdf.rb0000644000004100000410000011041012757133666016500 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end require 'origami/object' require 'origami/null' require 'origami/name' require 'origami/dictionary' require 'origami/reference' require 'origami/boolean' require 'origami/numeric' require 'origami/string' require 'origami/array' require 'origami/stream' require 'origami/tree' require 'origami/filters' require 'origami/header' require 'origami/metadata' require 'origami/functions' require 'origami/page' require 'origami/font' require 'origami/graphics' require 'origami/destinations' require 'origami/filespec' require 'origami/xfa' require 'origami/acroform' require 'origami/annotations' require 'origami/actions' require 'origami/3d' require 'origami/signature' require 'origami/webcapture' require 'origami/export' require 'origami/webcapture' require 'origami/encryption' require 'origami/linearization' require 'origami/obfuscation' require 'origami/javascript' require 'origami/outline' require 'origami/outputintents' require 'origami/collections' require 'origami/catalog' require 'origami/xreftable' require 'origami/trailer' require 'origami/parsers/pdf/linear' require 'origami/parsers/pdf/lazy' module Origami class InvalidPDFError < Error #:nodoc: end # # Main class representing a PDF file and its inner contents. # A PDF file contains a set of Revision. # class PDF # # Class representing a particular revision in a PDF file. # Revision contains : # * A Body, which is a sequence of Object. # * A XRef::Section, holding XRef information about objects in body. # * A Trailer. # class Revision attr_accessor :pdf attr_accessor :body, :xreftable, :xrefstm attr_reader :trailer def initialize(doc) @document = doc @body = {} @xreftable = nil @xrefstm = nil @trailer = nil end def trailer=(trl) trl.document = @document @trailer = trl end def has_xreftable? not @xreftable.nil? end def has_xrefstm? not @xrefstm.nil? end def each_object(&b) @body.each_value(&b) end def objects @body.values end end # # Document header and revisions. # attr_accessor :header, :revisions class << self # # Reads and parses a PDF file from disk. # def read(path, options = {}) path = File.expand_path(path) if path.is_a?(::String) lazy = options[:lazy] if lazy parser_class = PDF::LazyParser else parser_class = PDF::LinearParser end parser_class.new(options).parse(path) end # # Creates a new PDF and saves it. # If a block is passed, the PDF instance can be processed before saving. # def create(output, options = {}) pdf = PDF.new yield(pdf) if block_given? pdf.save(output, options) end alias write create # # Deserializes a PDF dump. # def deserialize(filename) Zlib::GzipReader.open(filename) { |gz| return Marshal.load(gz.read) } end end # # Creates a new PDF instance. # _parser_:: The Parser object creating the document. # If none is specified, some default structures are automatically created to get a minimal working document. # def initialize(parser = nil) @header = PDF::Header.new @revisions = [] add_new_revision @revisions.first.trailer = Trailer.new if parser @loaded = false @parser = parser else init end end # # Original file name if parsed from disk, nil otherwise. # def original_filename @parser.target_filename if @parser end # # Original file size if parsed from a data stream, nil otherwise. # def original_filesize @parser.target_filesize if @parser end # # Original data parsed to create this document, nil if created from scratch. # def original_data @parser.target_data if @parser end # # Serializes the current PDF. # def serialize(filename) parser = @parser @parser = nil # do not serialize the parser Zlib::GzipWriter.open(filename) { |gz| gz.write Marshal.dump(self) } @parser = parser self end # # Saves the current document. # _filename_:: The path where to save this PDF. # def save(path, params = {}) options = { delinearize: true, recompile: true, decrypt: false } options.update(params) if self.frozen? # incompatible flags with frozen doc (signed) options[:recompile] = options[:rebuild_xrefs] = options[:noindent] = options[:obfuscate] = false end if path.respond_to?(:write) fd = path else path = File.expand_path(path) fd = File.open(path, 'w').binmode close = true end load_all_objects unless @loaded intents_as_pdfa1 if options[:intent] =~ /pdf[\/-]?A1?/i self.delinearize! if options[:delinearize] and self.linearized? compile(options) if options[:recompile] fd.write output(options) fd.close if close self end alias write save # # Saves the file up to given revision number. # This can be useful to visualize the modifications over different incremental updates. # _revision_:: The revision number to save. # _filename_:: The path where to save this PDF. # def save_upto(revision, filename) save(filename, up_to_revision: revision) end # # Returns an array of strings, names and streams matching the given pattern. # _streams_: Search into decoded stream data. # _object_streams_: Search into objects inside object streams. # def grep(pattern, streams: true, object_streams: true) #:nodoc: pattern = /#{Regexp.escape(pattern)}/i if pattern.is_a?(::String) raise TypeError, "Expected a String or Regexp" unless pattern.is_a?(Regexp) result = [] search_object = -> (object) do case object when Stream result.concat object.dictionary.strings_cache.select{|str| pattern === str} result.concat object.dictionary.names_cache.select{|name| pattern === name.value} begin result.push object if streams and object.data.match(pattern) rescue Filter::Error next # Skip object if a decoding error occured. end next if object.is_a?(ObjectStream) and not object_streams object.each do |subobject| search_object.call(subobject) end when Name, String result.push object if object.value.match(pattern) when Dictionary, Array then result.concat object.strings_cache.select{|str| pattern === str} result.concat object.names_cache.select{|name| pattern === name.value} end end self.indirect_objects.each do |object| search_object.call(object) end result end # # Returns an array of Objects whose name (in a Dictionary) is matching _pattern_. # def ls(pattern, follow_references: true) pattern = /#{Regexp.escape(pattern)}/i if pattern.is_a?(::String) raise TypeError, "Expected a String or Regexp" unless pattern.is_a?(Regexp) self.grep(pattern, streams: false, object_streams: true) .select {|object| object.is_a?(Name) and object.parent.is_a?(Dictionary) and object.parent.key?(object) } .collect {|object| result = object.parent[object]; follow_references ? result.solve : result } end # # Iterates over the objects of the document. # _compressed_: iterates over the objects inside object streams. # _recursive_: iterates recursively inside objects like arrays and dictionaries. # def each_object(compressed: false, recursive: false) return enum_for(__method__, compressed: compressed, recursive: recursive ) unless block_given? walk_object = -> (object) do case object when Dictionary object.each_value do |value| yield(value) walk_object.call(value) end when Array object.each do |child| yield(child) walk_object.call(child) end when Stream yield(object.dictionary) walk_object.call(object.dictionary) end end @revisions.each do |revision| revision.each_object do |object| yield(object) walk_object.call(object) if recursive if object.is_a?(ObjectStream) and compressed object.each do |child_obj| yield(child_obj) walk_object.call(child_obj) if recursive end end end end end # # Return an array of indirect objects. # def indirect_objects @revisions.inject([]) do |set, rev| set.concat(rev.objects) end end alias root_objects indirect_objects # # Adds a new object to the PDF file. # If this object has no version number, then a new one will be automatically # computed and assignated to him. # # It returns a Reference to this Object. # _object_:: The object to add. # def <<(object) owner = object.document # # Does object belongs to another PDF ? # if owner and not owner.equal?(self) import object else add_to_revision(object, @revisions.last) end end alias insert << # # Similar to PDF#insert or PDF#<<, but for an object belonging to another document. # Object will be recursively copied and new version numbers will be assigned. # Returns the new reference to the imported object. # _object_:: The object to import. # def import(object) self.insert(object.export) end # # Adds a new object to a specific revision. # If this object has no version number, then a new one will be automatically # computed and assignated to him. # # It returns a Reference to this Object. # _object_:: The object to add. # _revision_:: The revision to add the object to. # def add_to_revision(object, revision) object.set_indirect(true) object.set_document(self) object.no, object.generation = allocate_new_object_number if object.no == 0 revision.body[object.reference] = object object.reference end # # Ends the current Revision, and starts a new one. # def add_new_revision root = @revisions.last.trailer[:Root] unless @revisions.empty? @revisions << Revision.new(self) @revisions.last.trailer = Trailer.new @revisions.last.trailer.Root = root self end # # Removes a whole document revision. # _index_:: Revision index, first is 0. # def remove_revision(index) if index < 0 or index > @revisions.size raise IndexError, "Not a valid revision index" end if @revisions.size == 1 raise InvalidPDFError, "Cannot remove last revision" end @revisions.delete_at(index) self end # # Looking for an object present at a specified file offset. # def get_object_by_offset(offset) #:nodoc: self.indirect_objects.find { |obj| obj.file_offset == offset } end # # Remove an object. # def delete_object(no, generation = 0) case no when Reference target = no when ::Integer target = Reference.new(no, generation) else raise TypeError, "Invalid parameter type : #{no.class}" end @revisions.each do |rev| rev.body.delete(target) end end # # Search for an indirect object in the document. # _no_:: Reference or number of the object. # _generation_:: Object generation. # def get_object(no, generation = 0, use_xrefstm: true) #:nodoc: case no when Reference target = no when ::Integer target = Reference.new(no, generation) when Origami::Object return no else raise TypeError, "Invalid parameter type : #{no.class}" end # # Search through accessible indirect objects. # @revisions.reverse_each do |rev| return rev.body[target] if rev.body.include?(target) end # # Search through xref sections. # @revisions.reverse_each do |rev| next unless rev.has_xreftable? xref = rev.xreftable.find(target.refno) next if xref.nil? or xref.free? # Try loading the object if it is not present. object = load_object_at_offset(rev, xref.offset) return object unless object.nil? end return nil unless use_xrefstm # Search through xref streams. @revisions.reverse_each do |rev| next unless rev.has_xrefstm? xrefstm = rev.xrefstm xref = xrefstm.find(target.refno) next if xref.nil? # # We found a matching XRef. # if xref.is_a?(XRefToCompressedObj) objstm = get_object(xref.objstmno, 0, use_xrefstm: use_xrefstm) object = objstm.extract_by_index(xref.index) if object.is_a?(Origami::Object) and object.no == target.refno return object else return objstm.extract(target.refno) end elsif xref.is_a?(XRef) object = load_object_at_offset(rev, xref.offset) return object unless object.nil? end end # # Lastly search directly into Object streams (might be very slow). # @revisions.reverse_each do |rev| stream = rev.objects.find{|obj| obj.is_a?(ObjectStream) and obj.include?(target.refno)} return stream.extract(target.refno) unless stream.nil? end nil end alias [] get_object # # Casts a PDF object into another object type. # The target type must be a subtype of the original type. # def cast_object(reference, type, parser = nil) #:nodoc: @revisions.each do |rev| if rev.body.include?(reference) and type < rev.body[reference].class rev.body[reference] = rev.body[reference].cast_to(type, parser) rev.body[reference] else nil end end end # # Returns a new number/generation for future object. # def allocate_new_object_number no = 1 # Deprecated number allocation policy (first available) #no = no + 1 while get_object(no) objset = self.indirect_objects self.indirect_objects.find_all{|obj| obj.is_a?(ObjectStream)}.each do |objstm| objstm.each{|obj| objset << obj} end allocated = objset.collect{|obj| obj.no}.compact no = allocated.max + 1 unless allocated.empty? [ no, 0 ] end # # Mark the document as complete. # No more objects needs to be fetched by the parser. # def loaded! @loaded = true end ########################## private ########################## # # Load an object from its given file offset. # The document must have an associated Parser. # def load_object_at_offset(revision, offset) return nil if @loaded or @parser.nil? pos = @parser.pos begin object = @parser.parse_object(offset) return nil if object.nil? if self.is_a?(Encryption::EncryptedDocument) case object when String object.extend(Encryption::EncryptedString) object.decrypted = false when Stream object.extend(Encryption::EncryptedStream) object.decrypted = false when Dictionary, Array object.strings_cache.each do |string| string.extend(Encryption::EncryptedString) string.decrypted = false end end end add_to_revision(object, revision) ensure @parser.pos = pos end object end # # Force the loading of all objects in the document. # def load_all_objects return if @loaded or @parser.nil? @revisions.each do |revision| if revision.has_xreftable? xrefs = revision.xreftable elsif revision.has_xrefstm? xrefs = revision.xrefstm else next end xrefs.each_with_number do |_, no| self.get_object(no) end end @loaded = true end # # Compute and update XRef::Section for each Revision. # def rebuild_xrefs size = 0 startxref = @header.to_s.size @revisions.each do |revision| revision.objects.each do |object| startxref += object.to_s.size end size += revision.body.size revision.xreftable = build_xrefs(revision.objects) revision.trailer ||= Trailer.new revision.trailer.Size = size + 1 revision.trailer.startxref = startxref startxref += revision.xreftable.to_s.size + revision.trailer.to_s.size end self end # # This method is meant to recompute, verify and correct main PDF structures, in order to output a proper file. # * Allocates objects references. # * Sets some objects missing required values. # def compile(options = {}) load_all_objects unless @loaded # # A valid document must have at least one page. # append_page if pages.empty? # # Allocates object numbers and creates references. # Invokes object finalization methods. # if self.is_a?(Encryption::EncryptedDocument) physicalize(options) else physicalize end # # Sets the PDF version header. # version, level = version_required @header.major_version = version[0,1].to_i @header.minor_version = version[2,1].to_i set_extension_level(version, level) if level > 0 self end # # Cleans the document from its references. # Indirects objects are made direct whenever possible. # TODO: Circuit-checking to avoid infinite induction # def logicalize #:nodoc: raise NotImplementedError processed = [] convert = -> (root) do replaced = [] if root.is_a?(Dictionary) or root.is_a?(Array) root.each do |obj| convert[obj] end root.map! do |obj| if obj.is_a?(Reference) target = obj.solve # Streams can't be direct objects if target.is_a?(Stream) obj else replaced << obj target end else obj end end end replaced end @revisions.each do |revision| revision.objects.each do |obj| processed.concat(convert[obj]) end end end # # Converts a logical PDF view into a physical view ready for writing. # def physicalize # # Indirect objects are added to the revision and assigned numbers. # build = -> (obj, revision) do # # Finalize any subobjects before building the stream. # if obj.is_a?(ObjectStream) obj.each do |subobj| build.call(subobj, revision) end end obj.pre_build if obj.is_a?(Dictionary) or obj.is_a?(Array) obj.map! do |subobj| if subobj.indirect? if get_object(subobj.reference) subobj.reference else ref = add_to_revision(subobj, revision) build.call(subobj, revision) ref end else subobj end end obj.each do |subobj| build.call(subobj, revision) end elsif obj.is_a?(Stream) build.call(obj.dictionary, revision) end obj.post_build end indirect_objects_by_rev.each do |obj, revision| build.call(obj, revision) end self end # # Returns the final binary representation of the current document. # def output(params = {}) has_objstm = self.indirect_objects.any?{|obj| obj.is_a?(ObjectStream)} options = { rebuild_xrefs: true, noindent: false, obfuscate: false, use_xrefstm: has_objstm, use_xreftable: (not has_objstm), up_to_revision: @revisions.size } options.update(params) options[:up_to_revision] = @revisions.size if options[:up_to_revision] > @revisions.size # Reset to default params if no xrefs are chosen (hybrid files not supported yet) if options[:use_xrefstm] == options[:use_xreftable] options[:use_xrefstm] = has_objstm options[:use_xreftable] = (not has_objstm) end # Get trailer dictionary trailer_info = get_trailer_info raise InvalidPDFError, "No trailer information found" if trailer_info.nil? trailer_dict = trailer_info.dictionary prev_xref_offset = nil xrefstm_offset = nil # Header bin = "" bin << @header.to_s # For each revision @revisions[0, options[:up_to_revision]].each do |rev| # Create xref table/stream. if options[:rebuild_xrefs] == true lastno_table, lastno_stm = 0, 0 brange_table, brange_stm = 0, 0 xrefs_stm = [ XRef.new(0, 0, XRef::FREE) ] xrefs_table = [ XRef.new(0, XRef::FIRSTFREE, XRef::FREE) ] if options[:use_xreftable] == true xrefsection = XRef::Section.new end if options[:use_xrefstm] == true xrefstm = rev.xrefstm || XRefStream.new if xrefstm == rev.xrefstm xrefstm.clear else add_to_revision(xrefstm, rev) end end end objset = rev.objects objset.find_all{|obj| obj.is_a?(ObjectStream)}.each do |objstm| objset.concat objstm.objects end if options[:rebuild_xrefs] == true and options[:use_xrefstm] == true previous_obj = nil # For each object, in number order # Move any XRefStream to the end of the revision. objset.sort_by {|obj| [obj.is_a?(XRefStream) ? 1 : 0, obj.no, obj.generation] } .each do |obj| # Ensures that every object has a unique reference number. # Duplicates should never happen in a well-formed revision and will cause breakage of xrefs. if previous_obj and previous_obj.reference == obj.reference raise InvalidPDFError, "Duplicate object detected, reference #{obj.reference}" else previous_obj = obj end # Create xref entry. if options[:rebuild_xrefs] == true # Adding subsections if needed if options[:use_xreftable] and (obj.no - lastno_table).abs > 1 xrefsection << XRef::Subsection.new(brange_table, xrefs_table) xrefs_table.clear brange_table = obj.no end if options[:use_xrefstm] and (obj.no - lastno_stm).abs > 1 xrefs_stm.each do |xref| xrefstm << xref end xrefstm.Index ||= [] xrefstm.Index << brange_stm << xrefs_stm.length xrefs_stm.clear brange_stm = obj.no end # Process embedded objects if options[:use_xrefstm] and obj.parent != obj and obj.parent.is_a?(ObjectStream) index = obj.parent.index(obj.no) xrefs_stm << XRefToCompressedObj.new(obj.parent.no, index) lastno_stm = obj.no else xrefs_stm << XRef.new(bin.size, obj.generation, XRef::USED) xrefs_table << XRef.new(bin.size, obj.generation, XRef::USED) lastno_table = lastno_stm = obj.no end end if obj.parent == obj or not obj.parent.is_a?(ObjectStream) # Finalize XRefStm if options[:rebuild_xrefs] == true and options[:use_xrefstm] == true and obj == xrefstm xrefstm_offset = bin.size xrefs_stm.each do |xref| xrefstm << xref end xrefstm.W = [ 1, (xrefstm_offset.to_s(2).size + 7) >> 3, 2 ] if xrefstm.DecodeParms.is_a?(Dictionary) and xrefstm.DecodeParms.has_key?(:Columns) xrefstm.DecodeParms[:Columns] = xrefstm.W[0] + xrefstm.W[1] + xrefstm.W[2] end xrefstm.Index ||= [] xrefstm.Index << brange_stm << xrefs_stm.size xrefstm.dictionary = xrefstm.dictionary.merge(trailer_dict) xrefstm.Prev = prev_xref_offset rev.trailer.dictionary = nil add_to_revision(xrefstm, rev) xrefstm.pre_build xrefstm.post_build end # Output object code if (obj.is_a?(Dictionary) or obj.is_a?(Stream)) and options[:noindent] bin << obj.to_s(indent: 0) else bin << obj.to_s end end end # end each object rev.trailer ||= Trailer.new # XRef table if options[:rebuild_xrefs] == true if options[:use_xreftable] == true table_offset = bin.size xrefsection << XRef::Subsection.new(brange_table, xrefs_table) rev.xreftable = xrefsection rev.trailer.dictionary = trailer_dict rev.trailer.Size = objset.size + 1 rev.trailer.Prev = prev_xref_offset rev.trailer.XRefStm = xrefstm_offset if options[:use_xrefstm] == true end startxref = options[:use_xreftable] == true ? table_offset : xrefstm_offset rev.trailer.startxref = prev_xref_offset = startxref end # Trailer bin << rev.xreftable.to_s if options[:use_xreftable] == true bin << (options[:obfuscate] == true ? rev.trailer.to_obfuscated_str : rev.trailer.to_s) end # end each revision bin end # # Instanciates basic structures required for a valid PDF file. # def init catalog = (self.Catalog = (trailer_key(:Root) || Catalog.new)) catalog.Pages = PageTreeNode.new.set_indirect(true) @revisions.last.trailer.Root = catalog.reference @loaded = true self end def filesize #:nodoc: output(rebuild_xrefs: false).size end def version_required #:nodoc: max = [ 1.0, 0 ] @revisions.each do |revision| revision.objects.each do |object| current = object.version_required max = current if (current <=> max) > 0 end end max[0] = max[0].to_s max end def indirect_objects_by_rev #:nodoc: @revisions.inject([]) do |set,rev| objset = rev.objects set.concat(objset.zip(::Array.new(objset.length, rev))) end end # # Compute and update XRef::Section for each Revision. # def rebuild_dummy_xrefs #:nodoc build_dummy_xrefs = -> (objects) do lastno = 0 brange = 0 xrefs = [ XRef.new(0, XRef::FIRSTFREE, XRef::FREE) ] xrefsection = XRef::Section.new objects.sort.each do |object| if (object.no - lastno).abs > 1 xrefsection << XRef::Subsection.new(brange, xrefs) brange = object.no xrefs.clear end xrefs << XRef.new(0, 0, XRef::FREE) lastno = object.no end xrefsection << XRef::Subsection.new(brange, xrefs) xrefsection end size = 0 startxref = @header.to_s.size @revisions.each do |revision| revision.objects.each do |object| startxref += object.to_s.size end size += revision.body.size revision.xreftable = build_dummy_xrefs.call(revision.objects) revision.trailer ||= Trailer.new revision.trailer.Size = size + 1 revision.trailer.startxref = startxref startxref += revision.xreftable.to_s.size + revision.trailer.to_s.size end self end # # Build a xref section from a set of objects. # def build_xrefs(objects) #:nodoc: lastno = 0 brange = 0 xrefs = [ XRef.new(0, XRef::FIRSTFREE, XRef::FREE) ] xrefsection = XRef::Section.new objects.sort.each do |object| if (object.no - lastno).abs > 1 xrefsection << XRef::Subsection.new(brange, xrefs) brange = object.no xrefs.clear end xrefs << XRef.new(get_object_offset(object.no, object.generation), object.generation, XRef::USED) lastno = object.no end xrefsection << XRef::Subsection.new(brange, xrefs) xrefsection end def delete_revision(ngen) #:nodoc: @revisions.delete_at[ngen] end def get_revision(ngen) #:nodoc: @revisions[ngen].body end def get_object_offset(no,generation) #:nodoc: objectoffset = @header.to_s.size @revisions.each do |revision| revision.objects.sort.each do |object| if object.no == no and object.generation == generation then return objectoffset else objectoffset += object.to_s.size end end objectoffset += revision.xreftable.to_s.size objectoffset += revision.trailer.to_s.size end nil end end end origami-2.0.0/lib/origami/font.rb0000644000004100000410000001755112757133666016711 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end module Origami # # Embedded font stream. # class FontStream < Stream field :Subtype, :Type => Name field :Length1, :Type => Integer field :Length2, :Type => Integer field :Length3, :Type => Integer field :Metadata, :Type => MetadataStream end # # Class representing a font details in a document. # class FontDescriptor < Dictionary include StandardObject FIXEDPITCH = 1 << 1 SERIF = 1 << 2 SYMBOLIC = 1 << 3 SCRIPT = 1 << 4 NONSYMBOLIC = 1 << 6 ITALIC = 1 << 7 ALLCAP = 1 << 17 SMALLCAP = 1 << 18 FORCEBOLD = 1 << 19 field :Type, :Type => Name, :Default => :FontDescriptor, :Required => true field :FontName, :Type => Name, :Required => true field :FontFamily, :Type => String, :Version => "1.5" field :FontStretch, :Type => Name, :Default => :Normal, :Version => "1.5" field :FontWeight, :Type => Integer, :Default => 400, :Version => "1.5" field :Flags, :Type => Integer, :Required => true field :FontBBox, :Type => Rectangle field :ItalicAngle, :Type => Number, :Required => true field :Ascent, :Type => Number field :Descent, :Type => Number field :Leading, :Type => Number, :Default => 0 field :CapHeight, :Type => Number field :XHeight, :Type => Number, :Default => 0 field :StemV, :Type => Number field :StemH, :Type => Number, :Default => 0 field :AvgWidth, :Type => Number, :Default => 0 field :MaxWidth, :Type => Number, :Default => 0 field :MissingWidth, :Type => Number, :Default => 0 field :FontFile, :Type => FontStream field :FontFile2, :Type => FontStream, :Version => "1.1" field :FontFile3, :Type => FontStream, :Version => "1.2" field :CharSet, :Type => String, :Version => "1.1" end # # Class representing a character encoding in a document. # class Encoding < Dictionary include StandardObject field :Type, :Type => Name, :Default => :Encoding field :BaseEncoding, :Type => Name field :Differences, :Type => Array end # # Class representing a rendering font in a document. # class Font < Dictionary include StandardObject field :Type, :Type => Name, :Default => :Font, :Required => true field :Subtype, :Type => Name, :Required => true field :Name, :Type => Name field :FirstChar, :Type => Integer field :LastChar, :Type => Integer field :Widths, :Type => Array.of(Number) field :FontDescriptor, :Type => FontDescriptor field :Encoding, :Type => [ Name, Encoding ], :Default => :MacRomanEncoding field :ToUnicode, :Type => Stream, :Version => "1.2" # TODO: Type0 and CID Fonts # # Type1 Fonts. # class Type1 < Font field :BaseFont, :Type => Name, :Required => true field :Subtype, :Type => Name, :Default => :Type1, :Required => true # # 14 standard Type1 fonts. # module Standard class TimesRoman < Type1 field :BaseFont, :Type => Name, :Default => :"Times-Roman", :Required => true end class Helvetica < Type1 field :BaseFont, :Type => Name, :Default => :Helvetica, :Required => true end class Courier < Type1 field :BaseFont, :Type => Name, :Default => :Courier, :Required => true end class Symbol < Type1 field :BaseFont, :Type => Name, :Default => :Symbol, :Required => true end class TimesBold < Type1 field :BaseFont, :Type => Name, :Default => :"Times-Bold", :Required => true end class HelveticaBold < Type1 field :BaseFont, :Type => Name, :Default => :"Helvetica-Bold", :Required => true end class CourierBold < Type1 field :BaseFont, :Type => Name, :Default => :"Courier-Bold", :Required => true end class ZapfDingbats < Type1 field :BaseFont, :Type => Name, :Default => :ZapfDingbats, :Required => true end class TimesItalic < Type1 field :BaseFont, :Type => Name, :Default => :"Times-Italic", :Required => true end class HelveticaOblique < Type1 field :BaseFont, :Type => Name, :Default => :"Helvetica-Oblique", :Required => true end class CourierOblique < Type1 field :BaseFont, :Type => Name, :Default => :"Courier-Oblique", :Required => true end class TimesBoldItalic < Type1 field :BaseFont, :Type => Name, :Default => :"Times-BoldItalic", :Required => true end class HelveticaBoldOblique < Type1 field :BaseFont, :Type => Name, :Default => :"Helvetica-BoldOblique", :Required => true end class CourierBoldOblique < Type1 field :BaseFont, :Type => Name, :Default => :"Courier-BoldOblique", :Required => true end end end # # TrueType Fonts # class TrueType < Font field :Subtype, :Type => Name, :Default => :TrueType, :Required => true field :BaseFont, :Type => Name, :Required => true end # # Type 3 Fonts # class Type3 < Font include ResourcesHolder field :Subtype, :Type => Name, :Default => :Type3, :Required => true field :FontBBox, :Type => Rectangle, :Required => true field :FontMatrix, :Type => Array.of(Number, length: 6), :Required => true field :CharProcs, :Type => Dictionary, :Required => true field :Resources, :Type => Resources, :Version => "1.2" end end end origami-2.0.0/lib/origami/filters/0000755000004100000410000000000012757133666017055 5ustar www-datawww-dataorigami-2.0.0/lib/origami/filters/runlength.rb0000644000004100000410000000724612757133666021421 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end module Origami module Filter class InvalidRunLengthDataError < DecodeError #:nodoc: end # # Class representing a Filter used to encode and decode data using RLE compression algorithm. # class RunLength include Filter EOD = 128 #:nodoc: # # Encodes data using RLE compression method. # _stream_:: The data to encode. # def encode(stream) result = "".b i = 0 while i < stream.size # # How many identical bytes coming? # length = 1 while i+1 < stream.size and length < EOD and stream[i] == stream[i+1] length = length + 1 i = i + 1 end # # If more than 1, then compress them. # if length > 1 result << (257 - length).chr << stream[i] # # Otherwise how many different bytes to copy? # else j = i while j+1 < stream.size and (j - i + 1) < EOD and stream[j] != stream[j+1] j = j + 1 end length = j - i result << length.chr << stream[i, length+1] i = j end i = i + 1 end result << EOD.chr end # # Decodes data using RLE decompression method. # _stream_:: The data to decode. # def decode(stream) result = "".b return result if stream.empty? i = 0 until i >= stream.length or stream[i].ord == EOD do # At least two bytes are required. if i > stream.length - 2 raise InvalidRunLengthDataError.new("Truncated run-length data", input_data: stream, decoded_data: result) end length = stream[i].ord if length < EOD result << stream[i + 1, length + 1] i = i + length + 2 else result << stream[i + 1] * (257 - length) i = i + 2 end end # Check if offset is beyond the end of data. if i >= stream.length raise InvalidRunLengthDataError.new("Truncated run-length data", input_data: stream, decoded_data: result) end result end end RL = RunLength end end origami-2.0.0/lib/origami/filters/lzw.rb0000644000004100000410000001717012757133666020224 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end require 'origami/filters/predictors' module Origami module Filter class InvalidLZWDataError < DecodeError #:nodoc: end # # Class representing a filter used to encode and decode data with LZW compression algorithm. # class LZW include Filter class DecodeParms < Dictionary include StandardObject field :Predictor, :Type => Integer, :Default => 1 field :Colors, :Type => Integer, :Default => 1 field :BitsPerComponent, :Type => Integer, :Default => 8 field :Columns, :Type => Integer, :Default => 1 field :EarlyChange, :Type => Integer, :Default => 1 end EOD = 257 #:nodoc: CLEARTABLE = 256 #:nodoc: # # Creates a new LZW Filter. # _parameters_:: A hash of filter options (ignored). # def initialize(parameters = {}) super(DecodeParms.new(parameters)) end # # Encodes given data using LZW compression method. # _stream_:: The data to encode. # def encode(string) if @params.Predictor.is_a?(Integer) colors = @params.Colors.is_a?(Integer) ? @params.Colors.to_i : 1 bpc = @params.BitsPerComponent.is_a?(Integer) ? @params.BitsPerComponent.to_i : 8 columns = @params.Columns.is_a?(Integer) ? @params.Columns.to_i : 1 string = Predictor.do_pre_prediction(string, predictor: @params.Predictor.to_i, colors: colors, bpc: bpc, columns: columns) end codesize = 9 result = Utils::BitWriter.new result.write(CLEARTABLE, codesize) table = clear({}) s = '' string.each_byte do |byte| char = byte.chr case table.size when 512 then codesize = 10 when 1024 then codesize = 11 when 2048 then codesize = 12 when 4096 result.write(CLEARTABLE, codesize) codesize = 9 clear table redo end it = s + char if table.has_key?(it) s = it else result.write(table[s], codesize) table[it] = table.size s = char end end result.write(table[s], codesize) result.write(EOD, codesize) result.final.to_s end # # Decodes given data using LZW compression method. # _stream_:: The data to decode. # def decode(string) result = "".b bstring = Utils::BitReader.new(string) codesize = 9 table = clear(Hash.new) prevbyte = nil until bstring.eod? do byte = bstring.read(codesize) case table.size when 510 then codesize = 10 when 1022 then codesize = 11 when 2046 then codesize = 12 when 4095 if byte != CLEARTABLE raise InvalidLZWDataError.new( "LZW table is full and no clear flag was set (codeword #{byte.to_s(2).rjust(codesize,'0')} at bit #{bstring.pos - codesize}/#{bstring.size})", input_data: string, decoded_data: result ) end end if byte == CLEARTABLE codesize = 9 clear table prevbyte = nil redo elsif byte == EOD break else if prevbyte.nil? raise InvalidLZWDataError.new( "No entry for codeword #{byte.to_s(2).rjust(codesize,'0')}.", input_data: string, decoded_data: result ) unless table.value?(byte) prevbyte = byte result << table.key(byte) redo else raise InvalidLZWDataError.new( "No entry for codeword #{prevbyte.to_s(2).rjust(codesize,'0')}.", input_data: string, decoded_data: result ) unless table.value?(prevbyte) if table.value?(byte) entry = table.key(byte) else entry = table.key(prevbyte) entry += entry[0,1] end result << entry table[table.key(prevbyte) + entry[0,1]] = table.size prevbyte = byte end end end if @params.Predictor.is_a?(Integer) colors = @params.Colors.is_a?(Integer) ? @params.Colors.to_i : 1 bpc = @params.BitsPerComponent.is_a?(Integer) ? @params.BitsPerComponent.to_i : 8 columns = @params.Columns.is_a?(Integer) ? @params.Columns.to_i : 1 result = Predictor.do_post_prediction(result, predictor: @params.Predictor.to_i, colors: colors, bpc: bpc, columns: columns) end result end private def clear(table) #:nodoc: table.clear 256.times do |i| table[i.chr] = i end table[CLEARTABLE] = CLEARTABLE table[EOD] = EOD table end end end end origami-2.0.0/lib/origami/filters/ccitt.rb0000644000004100000410000005526012757133666020520 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end module Origami module Filter class InvalidCCITTFaxDataError < DecodeError #:nodoc: end class CCITTFaxFilterError < Error #:nodoc: end # # Class representing a Filter used to encode and decode data with CCITT-facsimile compression algorithm. # class CCITTFax include Filter class DecodeParms < Dictionary include StandardObject field :K, :Type => Integer, :Default => 0 field :EndOfLine, :Type => Boolean, :Default => false field :EncodedByteAlign, :Type => Boolean, :Default => false field :Columns, :Type => Integer, :Default => 1728 field :Rows, :Type => Integer, :Default => 0 field :EndOfBlock, :Type => Boolean, :Default => true field :BlackIs1, :Type => Boolean, :Default => false field :DamagedRowsBeforeError, :Type => :Integer, :Default => 0 end def self.codeword(str) #:nodoc: [ str.to_i(2), str.length ] end EOL = codeword('000000000001') RTC = codeword('000000000001' * 6) WHITE_TERMINAL_ENCODE_TABLE = { 0 => codeword('00110101'), 1 => codeword('000111'), 2 => codeword('0111'), 3 => codeword('1000'), 4 => codeword('1011'), 5 => codeword('1100'), 6 => codeword('1110'), 7 => codeword('1111'), 8 => codeword('10011'), 9 => codeword('10100'), 10 => codeword('00111'), 11 => codeword('01000'), 12 => codeword('001000'), 13 => codeword('000011'), 14 => codeword('110100'), 15 => codeword('110101'), 16 => codeword('101010'), 17 => codeword('101011'), 18 => codeword('0100111'), 19 => codeword('0001100'), 20 => codeword('0001000'), 21 => codeword('0010111'), 22 => codeword('0000011'), 23 => codeword('0000100'), 24 => codeword('0101000'), 25 => codeword('0101011'), 26 => codeword('0010011'), 27 => codeword('0100100'), 28 => codeword('0011000'), 29 => codeword('00000010'), 30 => codeword('00000011'), 31 => codeword('00011010'), 32 => codeword('00011011'), 33 => codeword('00010010'), 34 => codeword('00010011'), 35 => codeword('00010100'), 36 => codeword('00010101'), 37 => codeword('00010110'), 38 => codeword('00010111'), 39 => codeword('00101000'), 40 => codeword('00101001'), 41 => codeword('00101010'), 42 => codeword('00101011'), 43 => codeword('00101100'), 44 => codeword('00101101'), 45 => codeword('00000100'), 46 => codeword('00000101'), 47 => codeword('00001010'), 48 => codeword('00001011'), 49 => codeword('01010010'), 50 => codeword('01010011'), 51 => codeword('01010100'), 52 => codeword('01010101'), 53 => codeword('00100100'), 54 => codeword('00100101'), 55 => codeword('01011000'), 56 => codeword('01011001'), 57 => codeword('01011010'), 58 => codeword('01011011'), 59 => codeword('01001010'), 60 => codeword('01001011'), 61 => codeword('00110010'), 62 => codeword('00110011'), 63 => codeword('00110100') } WHITE_TERMINAL_DECODE_TABLE = WHITE_TERMINAL_ENCODE_TABLE.invert BLACK_TERMINAL_ENCODE_TABLE = { 0 => codeword('0000110111'), 1 => codeword('010'), 2 => codeword('11'), 3 => codeword('10'), 4 => codeword('011'), 5 => codeword('0011'), 6 => codeword('0010'), 7 => codeword('00011'), 8 => codeword('000101'), 9 => codeword('000100'), 10 => codeword('0000100'), 11 => codeword('0000101'), 12 => codeword('0000111'), 13 => codeword('00000100'), 14 => codeword('00000111'), 15 => codeword('000011000'), 16 => codeword('0000010111'), 17 => codeword('0000011000'), 18 => codeword('0000001000'), 19 => codeword('00001100111'), 20 => codeword('00001101000'), 21 => codeword('00001101100'), 22 => codeword('00000110111'), 23 => codeword('00000101000'), 24 => codeword('00000010111'), 25 => codeword('00000011000'), 26 => codeword('000011001010'), 27 => codeword('000011001011'), 28 => codeword('000011001100'), 29 => codeword('000011001101'), 30 => codeword('000001101000'), 31 => codeword('000001101001'), 32 => codeword('000001101010'), 33 => codeword('000001101011'), 34 => codeword('000011010010'), 35 => codeword('000011010011'), 36 => codeword('000011010100'), 37 => codeword('000011010101'), 38 => codeword('000011010110'), 39 => codeword('000011010111'), 40 => codeword('000001101100'), 41 => codeword('000001101101'), 42 => codeword('000011011010'), 43 => codeword('000011011011'), 44 => codeword('000001010100'), 45 => codeword('000001010101'), 46 => codeword('000001010110'), 47 => codeword('000001010111'), 48 => codeword('000001100100'), 49 => codeword('000001100101'), 50 => codeword('000001010010'), 51 => codeword('000001010011'), 52 => codeword('000000100100'), 53 => codeword('000000110111'), 54 => codeword('000000111000'), 55 => codeword('000000100111'), 56 => codeword('000000101000'), 57 => codeword('000001011000'), 58 => codeword('000001011001'), 59 => codeword('000000101011'), 60 => codeword('000000101100'), 61 => codeword('000001011010'), 62 => codeword('000001100110'), 63 => codeword('000001100111') } BLACK_TERMINAL_DECODE_TABLE = BLACK_TERMINAL_ENCODE_TABLE.invert WHITE_CONFIGURATION_ENCODE_TABLE = { 64 => codeword('11011'), 128 => codeword('10010'), 192 => codeword('010111'), 256 => codeword('0110111'), 320 => codeword('00110110'), 384 => codeword('00110111'), 448 => codeword('01100100'), 512 => codeword('01100101'), 576 => codeword('01101000'), 640 => codeword('01100111'), 704 => codeword('011001100'), 768 => codeword('011001101'), 832 => codeword('011010010'), 896 => codeword('011010011'), 960 => codeword('011010100'), 1024 => codeword('011010101'), 1088 => codeword('011010110'), 1152 => codeword('011010111'), 1216 => codeword('011011000'), 1280 => codeword('011011001'), 1344 => codeword('011011010'), 1408 => codeword('011011011'), 1472 => codeword('010011000'), 1536 => codeword('010011001'), 1600 => codeword('010011010'), 1664 => codeword('011000'), 1728 => codeword('010011011'), 1792 => codeword('00000001000'), 1856 => codeword('00000001100'), 1920 => codeword('00000001001'), 1984 => codeword('000000010010'), 2048 => codeword('000000010011'), 2112 => codeword('000000010100'), 2176 => codeword('000000010101'), 2240 => codeword('000000010110'), 2340 => codeword('000000010111'), 2368 => codeword('000000011100'), 2432 => codeword('000000011101'), 2496 => codeword('000000011110'), 2560 => codeword('000000011111') } WHITE_CONFIGURATION_DECODE_TABLE = WHITE_CONFIGURATION_ENCODE_TABLE.invert BLACK_CONFIGURATION_ENCODE_TABLE = { 64 => codeword('0000001111'), 128 => codeword('000011001000'), 192 => codeword('000011001001'), 256 => codeword('000001011011'), 320 => codeword('000000110011'), 384 => codeword('000000110100'), 448 => codeword('000000110101'), 512 => codeword('0000001101100'), 576 => codeword('0000001101101'), 640 => codeword('0000001001010'), 704 => codeword('0000001001011'), 768 => codeword('0000001001100'), 832 => codeword('0000001001101'), 896 => codeword('0000001110010'), 960 => codeword('0000001110011'), 1024 => codeword('0000001110100'), 1088 => codeword('0000001110101'), 1152 => codeword('0000001110110'), 1216 => codeword('0000001110111'), 1280 => codeword('0000001010010'), 1344 => codeword('0000001010011'), 1408 => codeword('0000001010100'), 1472 => codeword('0000001010101'), 1536 => codeword('0000001011010'), 1600 => codeword('0000001011011'), 1664 => codeword('0000001100100'), 1728 => codeword('0000001100101'), 1792 => codeword('00000001000'), 1856 => codeword('00000001100'), 1920 => codeword('00000001001'), 1984 => codeword('000000010010'), 2048 => codeword('000000010011'), 2112 => codeword('000000010100'), 2176 => codeword('000000010101'), 2240 => codeword('000000010110'), 2340 => codeword('000000010111'), 2368 => codeword('000000011100'), 2432 => codeword('000000011101'), 2496 => codeword('000000011110'), 2560 => codeword('000000011111') } BLACK_CONFIGURATION_DECODE_TABLE = BLACK_CONFIGURATION_ENCODE_TABLE.invert # # Creates a new CCITT Fax Filter. # def initialize(parameters = {}) super(DecodeParms.new(parameters)) end # # Encodes data using CCITT-facsimile compression method. # def encode(stream) mode = @params.has_key?(:K) ? @params.K.value : 0 unless mode.is_a?(::Integer) and mode <= 0 raise NotImplementedError.new("CCITT encoding scheme not supported", input_data: stream) end columns = @params.has_key?(:Columns) ? @params.Columns.value : (stream.size << 3) unless columns.is_a?(::Integer) and columns > 0 #and columns % 8 == 0 raise CCITTFaxFilterError.new("Invalid value for parameter `Columns'", input_data: stream) end if stream.size % (columns >> 3) != 0 raise CCITTFaxFilterError.new("Data size is not a multiple of image width", input_data: stream) end colors = (@params.BlackIs1 == true) ? [0,1] : [1,0] white, _black = colors bitr = Utils::BitReader.new(stream) bitw = Utils::BitWriter.new # Group 4 requires an imaginary white line if mode < 0 prev_line = Utils::BitWriter.new write_bit_range(prev_line, white, columns) prev_line = Utils::BitReader.new(prev_line.final.to_s) end until bitr.eod? case when mode == 0 encode_one_dimensional_line(bitr, bitw, columns, colors) when mode < 0 encode_two_dimensional_line(bitr, bitw, columns, colors, prev_line) end end # Emit return-to-control code bitw.write(*RTC) bitw.final.to_s end # # Decodes data using CCITT-facsimile compression method. # def decode(stream) mode = @params.has_key?(:K) ? @params.K.value : 0 unless mode.is_a?(::Integer) and mode <= 0 raise NotImplementedError.new("CCITT encoding scheme not supported", input_data: stream) end columns = @params.has_key?(:Columns) ? @params.Columns.value : 1728 unless columns.is_a?(::Integer) and columns > 0 #and columns % 8 == 0 raise CCITTFaxFilterError.new("Invalid value for parameter `Columns'", input_data: stream) end colors = (@params.BlackIs1 == true) ? [0,1] : [1,0] white, _black = colors params = { is_aligned?: (@params.EncodedByteAlign == true), has_eob?: (@params.EndOfBlock.nil? or @params.EndOfBlock == true), has_eol?: (@params.EndOfLine == true) } unless params[:has_eob?] unless @params.has_key?(:Rows) and @params.Rows.is_a?(::Integer) and @params.Rows.value > 0 raise CCITTFaxFilterError.new("Invalid value for parameter `Rows'", input_data: stream) end rows = @params.Rows.to_i end bitr = Utils::BitReader.new(stream) bitw = Utils::BitWriter.new # Group 4 requires an imaginary white line if mode < 0 prev_line = Utils::BitWriter.new write_bit_range(prev_line, white, columns) prev_line = Utils::BitReader.new(prev_line.final.to_s) end until bitr.eod? or rows == 0 # realign the read line on a 8-bit boundary if required if params[:is_aligned?] and bitr.pos % 8 != 0 bitr.pos += 8 - (bitr.pos % 8) end # received return-to-control code if params[:has_eob?] and bitr.peek(RTC[1]) == RTC[0] bitr.pos += RTC[1] break end # checking for the presence of EOL if bitr.peek(EOL[1]) != EOL[0] raise InvalidCCITTFaxDataError.new( "No end-of-line pattern found (at bit pos #{bitr.pos}/#{bitr.size}})", input_data: stream, decoded_data: bitw.final.to_s ) if params[:has_eol?] else bitr.pos += EOL[1] end begin case when mode == 0 decode_one_dimensional_line(bitr, bitw, columns, colors) when mode < 0 decode_two_dimensional_line(bitr, bitw, columns, colors, prev_line) end rescue DecodeError => error error.input_data = stream error.decoded_data = bitw.final.to_s raise error end rows -= 1 unless params[:has_eob?] end bitw.final.to_s end private def encode_one_dimensional_line(input, output, columns, colors) #:nodoc: output.write(*EOL) scan_len = 0 white, _black = colors current_color = white # Process each bit in line. begin if input.read(1) == current_color scan_len += 1 else if current_color == white put_white_bits(output, scan_len) else put_black_bits(output, scan_len) end current_color ^= 1 scan_len = 1 end end while input.pos % columns != 0 if current_color == white put_white_bits(output, scan_len) else put_black_bits(output, scan_len) end # Align encoded lign on a 8-bit boundary. if @params.EncodedByteAlign == true and output.pos % 8 != 0 output.write(0, 8 - (output.pos % 8)) end end def encode_two_dimensional_line(input, output, columns, colors, prev_line) #:nodoc: raise NotImplementedError "CCITT two-dimensional encoding scheme not supported." end def decode_one_dimensional_line(input, output, columns, colors) #:nodoc: white, _black = colors current_color = white line_length = 0 while line_length < columns if current_color == white bit_length = get_white_bits(input) else bit_length = get_black_bits(input) end raise InvalidCCITTFaxDataError, "Unfinished line (at bit pos #{input.pos}/#{input.size}})" if bit_length.nil? line_length += bit_length raise InvalidCCITTFaxDataError, "Line is too long (at bit pos #{input.pos}/#{input.size}})" if line_length > columns write_bit_range(output, current_color, bit_length) current_color ^= 1 end end def decode_two_dimensional_line(input, output, columns, colors, prev_line) #:nodoc: raise NotImplementedError, "CCITT two-dimensional decoding scheme not supported." end def get_white_bits(bitr) #:nodoc: get_color_bits(bitr, WHITE_CONFIGURATION_DECODE_TABLE, WHITE_TERMINAL_DECODE_TABLE) end def get_black_bits(bitr) #:nodoc: get_color_bits(bitr, BLACK_CONFIGURATION_DECODE_TABLE, BLACK_TERMINAL_DECODE_TABLE) end def get_color_bits(bitr, config_words, term_words) #:nodoc: bits = 0 check_conf = true while check_conf check_conf = false (2..13).each do |length| codeword = bitr.peek(length) config_value = config_words[[codeword, length]] if config_value bitr.pos += length bits += config_value check_conf = true if config_value == 2560 break end end end (2..13).each do |length| codeword = bitr.peek(length) term_value = term_words[[codeword, length]] if term_value bitr.pos += length bits += term_value return bits end end nil end def lookup_bits(table, codeword, length) table.rassoc [codeword, length] end def put_white_bits(bitw, length) #:nodoc: put_color_bits(bitw, length, WHITE_CONFIGURATION_ENCODE_TABLE, WHITE_TERMINAL_ENCODE_TABLE) end def put_black_bits(bitw, length) #:nodoc: put_color_bits(bitw, length, BLACK_CONFIGURATION_ENCODE_TABLE, BLACK_TERMINAL_ENCODE_TABLE) end def put_color_bits(bitw, length, config_words, term_words) #:nodoc: while length > 2559 bitw.write(*config_words[2560]) length -= 2560 end if length > 63 conf_length = (length >> 6) << 6 bitw.write(*config_words[conf_length]) length -= conf_length end bitw.write(*term_words[length]) end def write_bit_range(bitw, bit_value, length) #:nodoc: bitw.write((bit_value << length) - bit_value, length) end end CCF = CCITTFax end end origami-2.0.0/lib/origami/filters/crypt.rb0000644000004100000410000000223712757133666020547 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end module Origami module Filter module Crypt # # Parameters for a Crypt Filter. # class DecodeParms < Dictionary include StandardObject field :Type, :Type => Name, :Default => :Crypt field :Name, :Type => Name, :Default => :Identity end end end end origami-2.0.0/lib/origami/filters/jbig2.rb0000644000004100000410000000325112757133666020400 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end module Origami module Filter # # Class representing a Filter used to encode and decode data with JBIG2 compression algorithm. # class JBIG2 include Filter class DecodeParms < Dictionary include StandardObject field :JBIG2Globals, :Type => Stream end def initialize(parameters = {}) super(DecodeParms.new(parameters)) end # # Not supported. # def encode(stream) raise NotImplementedError.new("#{self.class} is not yet supported", input_data: stream) end # # Not supported. # def decode(stream) raise NotImplementedError.new("#{self.class} is not yet supported", input_data: stream) end end end end origami-2.0.0/lib/origami/filters/dct.rb0000644000004100000410000000322112757133666020152 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end module Origami module Filter # # Class representing a Filter used to encode and decode data with DCT (JPEG) compression algorithm. # class DCT include Filter class DecodeParms < Dictionary include StandardObject field :ColorTransform, :Type => Integer end def initialize(parameters = {}) super(DecodeParms.new(parameters)) end def encode(stream) raise NotImplementedError.new("DCT filter is not supported", input_data: stream) end # # DCTDecode implies that data is a JPEG image container. # def decode(stream) raise NotImplementedError.new("DCT filter is not supported", input_data: stream) end end end end origami-2.0.0/lib/origami/filters/ascii.rb0000644000004100000410000001406612757133666020501 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end module Origami module Filter class InvalidASCIIHexStringError < DecodeError #:nodoc: end # # Class representing a filter used to encode and decode data written into hexadecimal. # class ASCIIHex include Filter EOD = ">" #:nodoc: # # Encodes given data into upcase hexadecimal representation. # _stream_:: The data to encode. # def encode(stream) stream.unpack("H*").join.upcase end # # Decodes given data writen into upcase hexadecimal representation. # _string_:: The data to decode. # def decode(string) input = string.include?(EOD) ? string[0...string.index(EOD)] : string digits = input.delete(" \f\t\r\n\0") # Ensure every digit is in the hexadecimal charset. unless digits =~ /^\h*$/ digits = digits.match(/^\h*/).to_s raise InvalidASCIIHexStringError.new("Invalid characters", input_data: string, decoded_data: [ digits ].pack('H*')) end [ digits ].pack "H*" end end AHx = ASCIIHex class InvalidASCII85StringError < DecodeError #:nodoc: end # # Class representing a filter used to encode and decode data written in base85 encoding. # class ASCII85 include Filter EOD = "~>" #:nodoc: # # Encodes given data into base85. # _stream_:: The data to encode. # def encode(stream) i = 0 code = "".b input = stream.dup while i < input.size do if input.length - i < 4 addend = 4 - (input.length - i) input << "\0" * addend else addend = 0 end inblock = (input[i].ord * 256**3 + input[i+1].ord * 256**2 + input[i+2].ord * 256 + input[i+3].ord) outblock = "" 5.times do |p| c = inblock / 85 ** (4 - p) outblock << ("!".ord + c).chr inblock -= c * 85 ** (4 - p) end outblock = "z" if outblock == "!!!!!" and addend == 0 if addend != 0 outblock = outblock[0,(4 - addend) + 1] end code << outblock i = i + 4 end code end # # Decodes the given data encoded in base85. # _string_:: The data to decode. # def decode(string) input = (string.include?(EOD) ? string[0..string.index(EOD) - 1] : string).delete(" \f\t\r\n\0") i = 0 result = "".b while i < input.size do outblock = "" if input[i] == "z" inblock = 0 codelen = 1 else inblock = 0 codelen = 5 if input.length - i < 5 raise InvalidASCII85StringError.new("Invalid length", input_data: string, decoded_data: result) if input.length - i == 1 addend = 5 - (input.length - i) input << "u" * addend else addend = 0 end # Checking if this string is in base85 5.times do |j| if input[i+j].ord > "u".ord or input[i+j].ord < "!".ord raise InvalidASCII85StringError.new( "Invalid character sequence: #{input[i,5].inspect}", input_data: string, decoded_data: result ) else inblock += (input[i+j].ord - "!".ord) * 85 ** (4 - j) end end raise InvalidASCII85StringError.new( "Invalid value (#{inblock}) for block #{input[i,5].inspect}", input_data: string, decoded_data: result ) if inblock >= 2**32 end 4.times do |p| c = inblock / 256 ** (3 - p) outblock << c.chr inblock -= c * 256 ** (3 - p) end if addend != 0 outblock = outblock[0, 4 - addend] end result << outblock i = i + codelen end result end end A85 = ASCII85 end end origami-2.0.0/lib/origami/filters/jpx.rb0000644000004100000410000000262712757133666020212 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end module Origami module Filter # # Class representing a Filter used to encode and decode data with JPX compression algorithm. # class JPX include Filter # # Not supported. # def encode(stream) raise NotImplementedError.new("#{self.class} is not yet supported", input_data: stream) end # # Not supported. # def decode(stream) raise NotImplementedError.new("#{self.class} is not yet supported", input_data: stream) end end end end origami-2.0.0/lib/origami/filters/flate.rb0000644000004100000410000001031612757133666020476 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end require 'zlib' require 'origami/filters/predictors' module Origami module Filter class InvalidFlateDataError < DecodeError; end #:nodoc: # # Class representing a Filter used to encode and decode data with zlib/Flate compression algorithm. # class Flate include Filter EOD = 257 #:nodoc: class DecodeParms < Dictionary include StandardObject field :Predictor, :Type => Integer, :Default => 1 field :Colors, :Type => Integer, :Default => 1 field :BitsPerComponent, :Type => Integer, :Default => 8 field :Columns, :Type => Integer, :Default => 1 end # # Create a new Flate Filter. # _parameters_:: A hash of filter options (ignored). # def initialize(parameters = {}) super(DecodeParms.new(parameters)) end # # Encodes data using zlib/Deflate compression method. # _stream_:: The data to encode. # def encode(stream) if @params.Predictor.is_a?(Integer) colors = @params.Colors.is_a?(Integer) ? @params.Colors.to_i : 1 bpc = @params.BitsPerComponent.is_a?(Integer) ? @params.BitsPerComponent.to_i : 8 columns = @params.Columns.is_a?(Integer) ? @params.Columns.to_i : 1 stream = Predictor.do_pre_prediction(stream, predictor: @params.Predictor.to_i, colors: colors, bpc: bpc, columns: columns) end Zlib::Deflate.deflate(stream, Zlib::BEST_COMPRESSION) end # # Decodes data using zlib/Inflate decompression method. # _stream_:: The data to decode. # def decode(stream) zlib_stream = Zlib::Inflate.new begin uncompressed = zlib_stream.inflate(stream) rescue Zlib::DataError => zlib_except uncompressed = zlib_stream.flush_next_out unless Origami::OPTIONS[:ignore_zlib_errors] raise InvalidFlateDataError.new(zlib_except.message, input_data: stream, decoded_data: uncompressed) end end if @params.Predictor.is_a?(Integer) colors = @params.Colors.is_a?(Integer) ? @params.Colors.to_i : 1 bpc = @params.BitsPerComponent.is_a?(Integer) ? @params.BitsPerComponent.to_i : 8 columns = @params.Columns.is_a?(Integer) ? @params.Columns.to_i : 1 uncompressed = Predictor.do_post_prediction(uncompressed, predictor: @params.Predictor.to_i, colors: colors, bpc: bpc, columns: columns) end uncompressed end end Fl = Flate end end origami-2.0.0/lib/origami/filters/predictors.rb0000644000004100000410000002244412757133666021566 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end module Origami module Filter class PredictorError < Error #:nodoc: end module Predictor NONE = 1 TIFF = 2 PNG_NONE = 10 PNG_SUB = 11 PNG_UP = 12 PNG_AVERAGE = 13 PNG_PAETH = 14 PNG_OPTIMUM = 15 def self.do_pre_prediction(data, predictor: NONE, colors: 1, bpc: 8, columns: 1) return data if predictor == NONE unless (1..4) === colors.to_i raise PredictorError.new("Colors must be between 1 and 4", input_data: data) end unless [1,2,4,8,16].include?(bpc.to_i) raise PredictorError.new("BitsPerComponent must be in 1, 2, 4, 8 or 16", input_data: data) end # components per line nvals = columns * colors # bytes per pixel bpp = (colors * bpc + 7) >> 3 # bytes per row bpr = (nvals * bpc + 7) >> 3 unless data.size % bpr == 0 raise PredictorError.new("Invalid data size #{data.size}, should be multiple of bpr=#{bpr}", input_data: data) end if predictor == TIFF do_tiff_pre_prediction(data, colors, bpc, columns) elsif predictor >= 10 # PNG do_png_pre_prediction(data, predictor, bpp, bpr) else raise PredictorError.new("Unknown predictor : #{predictor}", input_data: data) end end def self.do_post_prediction(data, predictor: NONE, colors: 1, bpc: 8, columns: 1) return data if predictor == NONE unless (1..4) === colors raise PredictorError.new("Colors must be between 1 and 4", input_data: data) end unless [1,2,4,8,16].include?(bpc) raise PredictorError.new("BitsPerComponent must be in 1, 2, 4, 8 or 16", input_data: data) end # components per line nvals = columns * colors # bytes per pixel bpp = (colors * bpc + 7) >> 3 # bytes per row bpr = ((nvals * bpc + 7) >> 3) + 1 if predictor == TIFF do_tiff_post_prediction(data, colors, bpc, columns) elsif predictor >= 10 # PNG do_png_post_prediction(data, bpp, bpr) else raise PredictorError.new("Unknown predictor : #{predictor}", input_data: data) end end def self.do_png_post_prediction(data, bpp, bpr) result = "" uprow = "\0" * bpr thisrow = "\0" * bpr nrows = (data.size + bpr - 1) / bpr nrows.times do |irow| line = data[irow * bpr, bpr] predictor = 10 + line[0].ord line[0] = "\0" for i in (1..line.size-1) up = uprow[i].ord if bpp > i left = upleft = 0 else left = line[i-bpp].ord upleft = uprow[i-bpp].ord end case predictor when PNG_NONE thisrow = line when PNG_SUB thisrow[i] = ((line[i].ord + left) & 0xFF).chr when PNG_UP thisrow[i] = ((line[i].ord + up) & 0xFF).chr when PNG_AVERAGE thisrow[i] = ((line[i].ord + ((left + up) / 2)) & 0xFF).chr when PNG_PAETH p = left + up - upleft pa, pb, pc = (p - left).abs, (p - up).abs, (p - upleft).abs thisrow[i] = ((line[i].ord + case [ pa, pb, pc ].min when pa then left when pb then up when pc then upleft end ) & 0xFF).chr else unless Origami::OPTIONS[:ignore_png_errors] raise PredictorError.new("Unknown PNG predictor : #{predictor}", input_data: data, decoded_data: result) end # behave as PNG_NONE thisrow = line end end result << thisrow[1..-1] uprow = thisrow end result end def self.do_png_pre_prediction(data, predictor, bpp, bpr) result = "" nrows = data.size / bpr line = "\0" + data[-bpr, bpr] (nrows-1).downto(0) do |irow| uprow = if irow == 0 "\0" * (bpr+1) else "\0" + data[(irow-1)*bpr,bpr] end bpr.downto(1) do |i| up = uprow[i].ord left = line[i-bpp].ord upleft = uprow[i-bpp].ord case predictor when PNG_SUB line[i] = ((line[i].ord - left) & 0xFF).chr when PNG_UP line[i] = ((line[i].ord - up) & 0xFF).chr when PNG_AVERAGE line[i] = ((line[i].ord - ((left + up) / 2)) & 0xFF).chr when PNG_PAETH p = left + up - upleft pa, pb, pc = (p - left).abs, (p - up).abs, (p - upleft).abs line[i] = ((line[i].ord - case [ pa, pb, pc ].min when pa then left when pb then up when pc then upleft end ) & 0xFF).chr when PNG_NONE else raise PredictorError.new("Unsupported PNG predictor : #{predictor}", input_data: data) end end line[0] = (predictor - 10).chr result = line + result line = uprow end result end def self.do_tiff_post_prediction(data, colors, bpc, columns) #:nodoc: bpr = (colors * bpc * columns + 7) >> 3 nrows = data.size / bpr bitmask = (1 << bpc) - 1 result = Utils::BitWriter.new nrows.times do |irow| line = Utils::BitReader.new(data[irow * bpr, bpr]) pixel = ::Array.new(colors, 0) columns.times do diffpixel = ::Array.new(colors) { line.read(bpc) } pixel = pixel.zip(diffpixel).map!{|c, diff| (c + diff) & bitmask} pixel.each do |c| result.write(c, bpc) end end result.final end result.final.to_s end def self.do_tiff_pre_prediction(data, colors, bpc, columns) #:nodoc: bpr = (colors * bpc * columns + 7) >> 3 nrows = data.size / bpr bitmask = (1 << bpc) - 1 result = Utils::BitWriter.new nrows.times do |irow| line = Utils::BitReader.new(data[irow * bpr, bpr]) diffpixel = ::Array.new(colors, 0) columns.times do pixel = ::Array.new(colors) { line.read(bpc) } diffpixel = diffpixel.zip(pixel).map!{|diff, c| (c - diff) & bitmask} diffpixel.each do |c| result.write(c, bpc) end end result.final end result.final.to_s end end end end origami-2.0.0/lib/origami/name.rb0000644000004100000410000000711012757133666016651 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end module Origami REGULARCHARS = "([^ \\t\\r\\n\\0\\[\\]<>()%\\/]|#[a-fA-F0-9][a-fA-F0-9])*" #:nodoc: class InvalidNameObjectError < InvalidObjectError #:nodoc: end # # Class representing a Name Object. # Name objects are strings which identify some PDF file inner structures. # class Name #< DelegateClass(Symbol) include Origami::Object TOKENS = %w{ / } #:nodoc: @@regexp = Regexp.new(WHITESPACES + TOKENS.first + "(?#{REGULARCHARS})" + WHITESPACES) #:nodoc # # Creates a new Name. # _name_:: A symbol representing the new Name value. # def initialize(name = "") unless name.is_a?(Symbol) or name.is_a?(::String) raise TypeError, "Expected type Symbol or String, received #{name.class}." end @value = name.to_s super() end def value @value.to_sym end def <=>(name) return unless name.is_a?(Name) self.value <=> name.value end def ==(object) #:nodoc: self.eql?(object) or @value.to_sym == object end def eql?(object) #:nodoc: object.is_a?(Name) and self.value.eql?(object.value) end def hash #:nodoc: @value.hash end def to_s #:nodoc: super(TOKENS.first + Name.expand(@value)) end def self.parse(stream, parser = nil) #:nodoc: offset = stream.pos name = if stream.scan(@@regexp).nil? raise InvalidNameObjectError, "Bad name format" else value = stream['name'] Name.new(value.include?('#') ? contract(value) : value) end name.file_offset = offset name end def self.contract(name) #:nodoc: i = 0 name = name.dup while i < name.length if name[i] == "#" digits = name[i+1, 2] unless /^[A-Za-z0-9]{2}$/ === digits raise InvalidNameObjectError, "Irregular use of # token" end char = digits.hex.chr if char == "\0" raise InvalidNameObjectError, "Null byte forbidden inside name definition" end name[i, 3] = char end i = i + 1 end name end def self.expand(name) #:nodoc: forbiddenchars = /[ #\t\r\n\0\[\]<>()%\/]/ name.gsub(forbiddenchars) do |c| "#" + c.ord.to_s(16).rjust(2,"0") end end def self.native_type ; Name end end end origami-2.0.0/lib/origami/annotations.rb0000644000004100000410000007616512757133666020306 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end module Origami # Forward declaration. class Action < Dictionary; end # # Class representing an annotation. # Annotations are objects which user can interact with. # class Annotation < Dictionary include StandardObject # # An AppearanceStream is a FormXObject. # class AppearanceStream < Graphics::FormXObject ; end # # Appearance Dictionary of an Annotation. # class AppearanceDictionary < Dictionary include StandardObject field :N, :Type => [ AppearanceStream, Dictionary ], :Required => true field :R, :Type => [ AppearanceStream, Dictionary ] field :D, :Type => [ AppearanceStream, Dictionary ] end # # Class representing additional actions which can be associated with an annotation having an AA field. # class AdditionalActions < Dictionary include StandardObject field :E, :Type => Action, :Version => "1.2" # Mouse Enter field :X, :Type => Action, :Version => "1.2" # Mouse Exit field :D, :Type => Action, :Version => "1.2" # Mouse Down field :U, :Type => Action, :Version => "1.2" # Mouse Up field :Fo, :Type => Action, :Version => "1.2" # Focus field :Bl, :Type => Action, :Version => "1.2" # Blur field :PO, :Type => Action, :Version => "1.2" # Page Open field :PC, :Type => Action, :Version => "1.2" # Page Close field :PV, :Type => Action, :Version => "1.2" # Page Visible field :PI, :Type => Action, :Version => "1.2" # Page Invisible end # # Annotation fields. # field :Type, :Type => Name, :Default => :Annot field :Subtype, :Type => Name, :Required => true field :Rect, :Type => Rectangle, :Default => [ 0, 0, 0, 0 ], :Required => true field :Contents, :Type => String field :P, :Type => Page, :Version => "1.3" field :NM, :Type => String, :Version => "1.4" field :M, :Type => String, :Version => "1.1" field :F, :Type => Integer, :Default => 0, :Version => "1.1" field :AP, :Type => AppearanceDictionary, :Version => "1.2" field :AS, :Type => Name, :Version => "1.2" field :Border, :Type => Array, :Default => [ 0 , 0 , 1 ] field :C, :Type => Array.of(Number), :Version => "1.1" field :StructParent, :Type => Integer, :Version => "1.3" field :OC, :Type => Dictionary, :Version => "1.5" def set_normal_appearance(apstm) self.AP ||= AppearanceDictionary.new self.AP[:N] = apstm self end def set_rollover_appearance(apstm) self.AP ||= AppearanceDictionary.new self.AP[:R] = apstm self end def set_down_appearance(apstm) self.AP ||= AppearanceStream.new self.AP[:D] = apstm self end module Triggerable def onMouseOver(action) unless action.is_a?(Action) or action.is_a?(Reference) raise TypeError, "An Action object must be passed." end self.AA ||= AdditionalActions.new self.AA.E = action end def onMouseOut(action) unless action.is_a?(Action) or action.is_a?(Reference) raise TypeError, "An Action object must be passed." end self.AA ||= AdditionalActions.new self.AA.X = action end def onMouseDown(action) unless action.is_a?(Action) or action.is_a?(Reference) raise TypeError, "An Action object must be passed." end self.AA ||= AdditionalActions.new self.AA.D = action end def onMouseUp(action) unless action.is_a?(Action) or action.is_a?(Reference) raise TypeError, "An Action object must be passed." end self.AA ||= AdditionalActions.new self.AA.U = action end def onFocus(action) unless action.is_a?(Action) or action.is_a?(Reference) raise TypeError, "An Action object must be passed." end self.AA ||= AdditionalActions.new self.AA.Fo = action end def onBlur(action) unless action.is_a?(Action) or action.is_a?(Reference) raise TypeError, "An Action object must be passed." end self.AA ||= AdditionalActions.new self.AA.Bl = action end def onPageOpen(action) unless action.is_a?(Action) or action.is_a?(Reference) raise TypeError, "An Action object must be passed." end self.AA ||= AdditionalActions.new self.AA.PO = action end def onPageClose(action) unless action.is_a?(Action) or action.is_a?(Reference) raise TypeError, "An Action object must be passed." end self.AA ||= AdditionalActions.new self.AA.PC = action end def onPageVisible(action) unless action.is_a?(Action) or action.is_a?(Reference) raise TypeError, "An Action object must be passed." end self.AA ||= AdditionalActions.new self.AA.PV = action end def onPageInvisible(action) unless action.is_a?(Action) or action.is_a?(Reference) raise TypeError, "An Action object must be passed." end self.AA ||= AdditionalActions.new self.AA.PI = action end end # # Annotation flags # module Flags INVISIBLE = 1 << 0 HIDDEN = 1 << 1 PRINT = 1 << 2 NOZOOM = 1 << 3 NOROTATE = 1 << 4 NOVIEW = 1 << 5 READONLY = 1 << 6 LOCKED = 1 << 7 TOGGLENOVIEW = 1 << 8 LOCKEDCONTENTS = 1 << 9 end module Markup def self.included(receiver) receiver.field :T, :Type => String, :Version => "1.1" receiver.field :Popup, :Type => Dictionary, :Version => "1.3" receiver.field :CA, :Type => Number, :Default => 1.0, :Version => "1.4" receiver.field :RC, :Type => [String, Stream], :Version => "1.5" receiver.field :CreationDate, :Type => String, :Version => "1.5" receiver.field :IRT, :Type => Dictionary, :Version => "1.5" receiver.field :Subj, :Type => String, :Version => "1.5" receiver.field :RT, :Type => Name, :Default => :R, :Version => "1.6" receiver.field :IT, :Type => Name, :Version => "1.6" receiver.field :ExData, :Type => Dictionary, :Version => "1.7" end end class BorderStyle < Dictionary include StandardObject SOLID = :S DASHED = :D BEVELED = :B INSET = :I UNDERLINE = :U field :Type, :Type => Name, :Default => :Border field :W, :Type => Number, :Default => 1 field :S, :Type => Name, :Default => SOLID field :D, :Type => Array, :Default => [ 3 ] end class BorderEffect < Dictionary include StandardObject NONE = :S CLOUDY = :C field :S, :Type => Name, :Default => NONE field :I, :Type => Integer, :Default => 0 end class AppearanceCharacteristics < Dictionary include StandardObject module CaptionStyle CAPTION_ONLY = 0 ICON_ONLY = 1 CAPTION_BELOW = 2 CAPTION_ABOVE = 3 CAPTION_RIGHT = 4 CAPTION_LEFT = 5 CAPTION_OVERLAID = 6 end field :R, :Type => Integer, :Default => 0 field :BC, :Type => Array.of(Number) field :BG, :Type => Array.of(Number) field :CA, :Type => String field :RC, :Type => String field :AC, :Type => String field :I, :Type => Stream field :RI, :Type => Stream field :IX, :Type => Stream field :IF, :Type => Dictionary field :TP, :Type => Integer, :Default => CaptionStyle::CAPTION_ONLY end class Shape < Annotation include Markup field :Subtype, :Type => Name, :Required => true field :BS, :Type => BorderStyle field :IC, :Type => Array.of(Number) field :BE, :Type => BorderEffect, :Version => "1.5" field :RD, :Type => Rectangle, :Version => "1.5" end class Square < Shape field :Subtype, :Type => Name, :Default => :Square, :Required => true end class Circle < Shape field :Subtype, :Type => Name, :Default => :Circle, :Required => true end # # Text annotation # class Text < Annotation include Markup module TextName COMMENT = :C KEY = :K NOTE = :N HELP = :H NEWPARAGRAPH = :NP PARAGRAPH = :P INSERT = :I end field :Subtype, :Type => Name, :Default => :Text, :Required => true field :Open, :Type => Boolean, :Default => false field :Name, :Type => Name, :Default => TextName::NOTE field :State, :Type => String, :Version => "1.5" field :StateModel, :Type => String, :Version => "1.5" def pre_build model = self.StateModel state = self.State case model when "Marked" self.State = "Unmarked" if state.nil? when "Review" self.State = "None" if state.nil? end super end end # # FreeText Annotation # class FreeText < Annotation include Markup module Intent FREETEXT = :FreeText FREETEXTCALLOUT = :FreeTextCallout FREETEXTTYPEWRITER = :FreeTextTypeWriter end field :Subtype, :Type => Name, :Default => :FreeText, :Required => true field :DA, :Type => String, :Default => "/F1 10 Tf 0 g", :Required => true field :Q, :Type => Integer, :Default => Field::TextAlign::LEFT, :Version => "1.4" field :RC, :Type => [String, Stream], :Version => "1.5" field :DS, :Type => String, :Version => "1.5" field :CL, :Type => Array.of(Number), :Version => "1.6" field :IT, :Type => Name, :Default => Intent::FREETEXT, :Version => "1.6" field :BE, :Type => BorderEffect, :Version => "1.6" field :RD, :Type => Rectangle, :Version => "1.6" field :BS, :Type => BorderStyle, :Version => "1.6" field :LE, :Type => Name, :Default => :None, :Version => "1.6" end # # Class representing an link annotation. # class Link < Annotation # # The annotations highlighting mode. # The visual effect to be used when the mouse button is pressed or held down inside its active area. # module Highlight # No highlighting NONE = :N # Invert the contents of the annotation rectangle. INVERT = :I # Invert the annotations border. OUTLINE = :O # Display the annotation as if it were being pushed below the surface of the page PUSH = :P end field :Subtype, :Type => Name, :Default => :Link, :Required => true field :A, :Type => Action, :Version => "1.1" field :Dest, :Type => [ Destination, Name, String ] field :H, :Type => Name, :Default => Highlight::INVERT, :Version => "1.2" field :PA, :Type => Dictionary, :Version => "1.3" field :QuadPoints, :Type => Array.of(Number), :Version => "1.6" field :BS, :Type => BorderStyle, :Version => "1.6" end # # Class representing a file attachment annotation. # class FileAttachment < Annotation include Markup # Icons to be displayed for file attachment. module Icons GRAPH = :Graph PAPERCLIP = :Paperclip PUSHPIN = :PushPin TAG = :Tag end field :Subtype, :Type => Name, :Default => :FileAttachment, :Required => true field :FS, :Type => FileSpec, :Required => true field :Name, :Type => Name, :Default => Icons::PUSHPIN end # # Class representing a screen Annotation. # A screen annotation specifies a region of a page upon which media clips may be played. It also serves as an object from which actions can be triggered. # class Screen < Annotation include Triggerable field :Subtype, :Type => Name, :Default => :Screen, :Required => true field :T, :Type => String field :MK, :Type => AppearanceCharacteristics field :A, :Type => Action, :Version => "1.1" field :AA, :Type => AdditionalActions, :Version => "1.2" end class Sound < Annotation include Markup module Icons SPEAKER = :Speaker MIC = :Mic end field :Subtype, :Type => Name, :Default => :Sound, :Required => true field :Sound, :Type => Stream, :Required => true field :Name, :Type => Name, :Default => Icons::SPEAKER end class RichMedia < Annotation class Position < Dictionary include StandardObject NEAR = :Near CENTER = :Center FAR = :Far field :Type, :Type => Name, :Default => :RichMediaPosition, :Version => "1.7", :ExtensionLevel => 3 field :HAlign, :Type => Name, :Default => FAR, :Version => "1.7", :ExtensionLevel => 3 field :VAlign, :Type => Name, :Default => NEAR, :Version => "1.7", :ExtensionLevel => 3 field :HOffset, :Type => Number, :Default => 18, :Version => "1.7", :ExtensionLevel => 3 field :VOffset, :Type => Number, :Default => 18, :Version => "1.7", :ExtensionLevel => 3 end class Window < Dictionary include StandardObject field :Type, :Type => Name, :Default => :RichMediaWindow, :Version => "1.7", :ExtensionLevel => 3 field :Width, :Type => Dictionary, :Default => {:Default => 288, :Max => 576, :Min => 72}, :Version => "1.7", :ExtensionLevel => 3 field :Height, :Type => Dictionary, :Default => {:Default => 216, :Max => 432, :Min => 72}, :Version => "1.7", :ExtensionLevel => 3 field :Position, :Type => Position, :Version => "1.7", :ExtensionLevel => 3 end class Presentation < Dictionary include StandardObject WINDOWED = :Windowed EMBEDDED = :Embedded field :Type, :Type => Name, :Default => :RichMediaPresentation, :Version => "1.7", :ExtensionLevel => 3 field :Style, :Type => Name, :Default => EMBEDDED, :Version => "1.7", :ExtensionLevel => 3 field :Window, :Type => Window, :Version => "1.7", :ExtensionLevel => 3 field :Transparent, :Type => Boolean, :Default => false, :Version => "1.7", :ExtensionLevel => 3 field :NavigationPane, :Type => Boolean, :Default => false, :Version => "1.7", :ExtensionLevel => 3 field :Toolbar, :Type => Boolean, :Version => "1.7", :ExtensionLevel => 3 field :PassContextClick, :Type => Boolean, :Default => false, :Version => "1.7", :ExtensionLevel => 3 end class Animation < Dictionary include StandardObject NONE = :None LINEAR = :Linear OSCILLATING = :Oscillating field :Type, :Type => Name, :Default => :RichMediaAnimation, :Version => "1.7", :ExtensionLevel => 3 field :Subtype, :Type => Name, :Default => NONE, :Version => "1.7", :ExtensionLevel => 3 field :PlayCount, :Type => Integer, :Default => -1, :Version => "1.7", :ExtensionLevel => 3 field :Speed, :Type => Number, :Default => 1, :Version => "1.7", :ExtensionLevel => 3 end class Activation < Dictionary include StandardObject USER_ACTION = :XA PAGE_OPEN = :PO PAGE_VISIBLE = :PV field :Type, :Type => Name, :Default => :RichMediaActivation, :Version => "1.7", :ExtensionLevel => 3 field :Condition, :Type => Name, :Default => USER_ACTION, :Version => "1.7", :ExtensionLevel => 3 field :Animation, :Type => Animation, :Version => "1.7", :ExtensionLevel => 3 field :View, :Type => Dictionary, :Version => "1.7", :ExtensionLevel => 3 field :Configuration, :Type => Dictionary, :Version => "1.7", :ExtensionLevel => 3 field :Presentation, :Type => Presentation, :Version => "1.7", :ExtensionLevel => 3 field :Scripts, :Type => Array.of(FileSpec), :Version => "1.7", :ExtensionLevel => 3 end class Deactivation < Dictionary include StandardObject USER_ACTION = :XD PAGE_CLOSE = :PC PAGE_INVISIBLE = :PV field :Type, :Type => Name, :Default => :RichMediaDeactivation, :Version => "1.7", :ExtensionLevel => 3 field :Condition, :Type => Name, :Default => USER_ACTION, :Version => "1.7", :ExtensionLevel => 3 end class Settings < Dictionary include StandardObject field :Type, :Type => Name, :Default => :RichMediaSettings, :Version => "1.7", :ExtensionLevel => 3 field :Activation, :Type => Activation, :Version => "1.7", :ExtensionLevel => 3 field :Deactivation, :Type => Deactivation, :Version => "1.7", :ExtensionLevel => 3 end class CuePoint < Dictionary include StandardObject NAVIGATION = :Navigation EVENT = :Event field :Type, :Type => Name, :Default => :CuePoint, :Version => "1.7", :ExtensionLevel => 3 field :Subtype, :Type => Name, :Version => "1.7", :ExtensionLevel => 3 field :Name, :Type => String, :Version => "1.7", :ExtensionLevel => 3, :Required => true field :Time, :Type => Number, :Version => "1.7", :ExtensionLevel => 3, :Required => true field :A, :Type => Action, :Version => "1.7", :ExtensionLevel => 3, :Required => true end class Parameters < Dictionary include StandardObject module Binding NONE = :None FOREGROUND = :Foreground BACKGROUND = :Background MATERIAL = :Material end field :Type, :Type => Name, :Default => :RichMediaParams, :Version => "1.7", :ExtensionLevel => 3 field :FlashVars, :Type => [String, Stream], :Version => "1.7", :ExtensionLevel => 3 field :Binding, :Type => Name, :Default => Binding::NONE, :Version => "1.7", :ExtensionLevel => 3 field :BindingMaterialName, :Type => String, :Version => "1.7", :ExtensionLevel => 3 field :CuePoints, :Type => Array.of(CuePoint), :Default => [], :Version => "1.7", :ExtensionLevel => 3 field :Settings, :Type => [String, Stream], :Version => "1.7", :ExtensionLevel => 3 end class Instance < Dictionary include StandardObject U3D = :"3D" FLASH = :Flash SOUND = :Sound VIDEO = :Video field :Type, :Type => Name, :Default => :RichMediaInstance, :Version => "1.7", :ExtensionLevel => 3 field :Subtype, :Type => Name, :Version => "1.7", :ExtensionLevel => 3 field :Params, :Type => Parameters, :Version => "1.7", :ExtensionLevel => 3 field :Asset, :Type => FileSpec, :Version => "1.7", :ExtensionLevel => 3 end class Configuration < Dictionary include StandardObject U3D = :"3D" FLASH = :Flash SOUND = :Sound VIDEO = :Video field :Type, :Type => Name, :Default => :RichMediaConfiguration, :Version => "1.7", :ExtensionLevel => 3 field :Subtype, :Type => Name, :Version => "1.7", :ExtensionLevel => 3 field :Name, :Type => String, :Version => "1.7", :ExtensionLevel => 3 field :Instances, :Type => Array.of(Instance), :Version => "1.7", :ExtensionLevel => 3 end class Content < Dictionary include StandardObject field :Type, :Type => Name, :Default => :RichMediaContent, :Version => "1.7", :ExtensionLevel => 3 field :Assets, :Type => Dictionary, :Version => "1.7", :ExtensionLevel => 3 field :Configurations, :Type => Array.of(Configuration), :Version => "1.7", :ExtensionLevel => 3 field :Views, :Type => Array, :Version => "1.7", :ExtensionLevel => 3 end # # Fields of the RichMedia Annotation. # field :Subtype, :Type => Name, :Default => :RichMedia, :Version => "1.7", :ExtensionLevel => 3, :Required => true field :RichMediaSettings, :Type => Settings, :Version => "1.7", :ExtensionLevel => 3 field :RichMediaContent, :Type => Content, :Version => "1.7", :ExtensionLevel => 3, :Required => true end # Added in ExtensionLevel 3. class Projection < Annotation; end # # Class representing a widget Annotation. # Interactive forms use widget annotations to represent the appearance of fields and to manage user interactions. # class Widget < Annotation include Field include Triggerable module Highlight # No highlighting NONE = :N # Invert the contents of the annotation rectangle. INVERT = :I # Invert the annotations border. OUTLINE = :O # Display the annotation as if it were being pushed below the surface of the page PUSH = :P # Same as P. TOGGLE = :T end field :Subtype, :Type => Name, :Default => :Widget, :Required => true field :H, :Type => Name, :Default => Highlight::INVERT field :MK, :Type => AppearanceCharacteristics field :A, :Type => Action, :Version => "1.1" field :AA, :Type => AdditionalActions, :Version => "1.2" field :BS, :Type => BorderStyle, :Version => "1.2" def onActivate(action) unless action.is_a?(Action) raise TypeError, "An Action object must be passed." end self.A = action end class Button < Widget module Flags NOTOGGLETOOFF = 1 << 14 RADIO = 1 << 15 PUSHBUTTON = 1 << 16 RADIOSINUNISON = 1 << 26 end field :FT, :Type => Name, :Default => Field::Type::BUTTON, :Required => true end class PushButton < Button def pre_build self.Ff ||= 0 self.Ff |= Button::Flags::PUSHBUTTON super end end class CheckBox < Button def pre_build self.Ff ||= 0 self.Ff &= ~Button::Flags::RADIO self.Ff &= ~Button::Flags::PUSHBUTTON super end end class Radio < Button def pre_build self.Ff ||= 0 self.Ff &= ~Button::Flags::PUSHBUTTON self.Ff |= Button::Flags::RADIO super end end class Text < Widget module Flags MULTILINE = 1 << 12 PASSWORD = 1 << 13 FILESELECT = 1 << 20 DONOTSPELLCHECK = 1 << 22 DONOTSCROLL = 1 << 23 COMB = 1 << 24 RICHTEXT = 1 << 25 end field :FT, :Type => Name, :Default => Field::Type::TEXT, :Required => true field :MaxLen, :Type => Integer end class Choice < Widget module Flags COMBO = 1 << 17 EDIT = 1 << 18 SORT = 1 << 19 MULTISELECT = 1 << 21 DONOTSPELLCHECK = 1 << 22 COMMITONSELCHANGE = 1 << 26 end field :FT, :Type => Name, :Default => Field::Type::CHOICE, :Required => true field :Opt, :Type => Array field :TI, :Type => Integer, :Default => 0 field :I, :Type => Array, :Version => "1.4" end class ComboBox < Choice def pre_build self.Ff ||= 0 self.Ff |= Choice::Flags::COMBO super end end class ListBox < Choice def pre_build self.Ff ||= 0 self.Ff &= ~Choice::Flags::COMBO super end end class Signature < Widget field :FT, :Type => Name, :Default => Field::Type::SIGNATURE field :Lock, :Type => SignatureLock, :Version => "1.5" field :SV, :Type => SignatureSeedValue, :Version => "1.5" end end end end origami-2.0.0/lib/origami/xreftable.rb0000644000004100000410000003626012757133666017715 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end module Origami class PDF # # Tries to strip any xrefs information off the document. # def remove_xrefs # Delete a XRefStream and its ancestors. delete_xrefstm = -> (xrefstm) do prev = xrefstm.Prev delete_object(xrefstm.reference) if prev.is_a?(Integer) and (prev_stm = get_object_by_offset(prev)).is_a?(XRefStream) delete_xrefstm.call(prev_stm) end end @revisions.reverse_each do |rev| if rev.has_xrefstm? delete_xrefstm.call(rev.xrefstm) end if rev.trailer.has_dictionary? and rev.trailer.XRefStm.is_a?(Integer) xrefstm = get_object_by_offset(rev.trailer.XRefStm) delete_xrefstm.call(xrefstm) if xrefstm.is_a?(XRefStream) end rev.xrefstm = rev.xreftable = nil end end end class InvalidXRefError < Error #:nodoc: end # # Class representing a Cross-reference information. # class XRef FREE = "f" USED = "n" FIRSTFREE = 65535 @@regexp = /(?\d{10}) (?\d{5}) (?n|f)(\r\n| \r| \n)/ attr_accessor :offset, :generation, :state # # Creates a new XRef. # _offset_:: The file _offset_ of the referenced Object. # _generation_:: The generation number of the referenced Object. # _state_:: The state of the referenced Object (FREE or USED). # def initialize(offset, generation, state) @offset, @generation, @state = offset, generation, state end def self.parse(stream) #:nodoc: if stream.scan(@@regexp).nil? raise InvalidXRefError, "Invalid XRef format" end offset = stream['offset'].to_i generation = stream['gen'].to_i state = stream['state'] XRef.new(offset, generation, state) end # # Returns true if the associated object is used. # def used? @state == USED end # # Returns true if the associated object is freed. # def free? @state == FREE end # # Outputs self into PDF code. # def to_s off = @offset.to_s.rjust(10, '0') gen = @generation.to_s.rjust(5, '0') "#{off} #{gen} #{@state}" + EOL end def to_xrefstm_data(type_w, field1_w, field2_w) type_w <<= 3 field1_w <<= 3 field2_w <<= 3 type = ((@state == FREE) ? "\000" : "\001").unpack("B#{type_w}")[0] offset = @offset.to_s(2).rjust(field1_w, '0') generation = @generation.to_s(2).rjust(field2_w, '0') [ type , offset, generation ].pack("B#{type_w}B#{field1_w}B#{field2_w}") end class InvalidXRefSubsectionError < Error #:nodoc: end # # Class representing a cross-reference subsection. # A subsection contains a continute set of XRef. # class Subsection @@regexp = Regexp.new("(?\\d+) (?\\d+)" + WHITESPACES + "(\\r?\\n|\\r\\n?)") attr_reader :range # # Creates a new XRef subsection. # _start_:: The number of the first object referenced in the subsection. # _entries_:: An array of XRef. # def initialize(start, entries = []) @entries = entries.dup @range = Range.new(start, start + entries.size - 1) end def self.parse(stream) #:nodoc: if stream.scan(@@regexp).nil? raise InvalidXRefSubsectionError, "Bad subsection format" end start = stream['start'].to_i size = stream['size'].to_i xrefs = [] size.times do xrefs << XRef.parse(stream) end XRef::Subsection.new(start, xrefs) end # # Returns whether this subsection contains information about a particular object. # _no_:: The Object number. # def has_object?(no) @range.include?(no) end # # Returns XRef associated with a given object. # _no_:: The Object number. # def [](no) @entries[no - @range.begin] end # # Processes each XRef in the subsection. # def each(&b) @entries.each(&b) end # # Processes each XRef in the subsection, passing the XRef and the object number to the block. # def each_with_number return enum_for(__method__) { self.size } unless block_given? counter = @range.to_enum @entries.each do |entry| yield(entry, counter.next) end end # # The number of entries in the subsection. # def size @entries.size end # # Outputs self into PDF code. # def to_s section = "#{@range.begin} #{@range.end - @range.begin + 1}" + EOL @entries.each do |xref| section << xref.to_s end section end end class InvalidXRefSectionError < Error #:nodoc: end # # Class representing a Cross-reference table. # A section contains a set of XRefSubsection. # class Section TOKEN = "xref" @@regexp_open = Regexp.new(WHITESPACES + TOKEN + WHITESPACES + "(\\r?\\n|\\r\\n?)") @@regexp_sub = Regexp.new("(\\d+) (\\d+)" + WHITESPACES + "(\\r?\\n|\\r\\n?)") # # Creates a new XRef section. # _subsections_:: An array of XRefSubsection. # def initialize(subsections = []) @subsections = subsections end def self.parse(stream) #:nodoc: if stream.skip(@@regexp_open).nil? raise InvalidXRefSectionError, "No xref token found" end subsections = [] while stream.match?(@@regexp_sub) do subsections << XRef::Subsection.parse(stream) end XRef::Section.new(subsections) end # # Appends a new subsection. # _subsection_:: A XRefSubsection. # def <<(subsection) @subsections << subsection end # # Returns a XRef associated with a given object. # _no_:: The Object number. # def [](no) @subsections.each do |s| return s[no] if s.has_object?(no) end nil end alias find [] # # Processes each XRef in each Subsection. # def each(&b) return enum_for(__method__) { self.size } unless block_given? @subsections.each do |subsection| subsection.each(&b) end end # # Processes each XRef in each Subsection, passing the XRef and the object number. # def each_with_number(&b) return enum_for(__method__) { self.size } unless block_given? @subsections.each do |subsection| subsection.each_with_number(&b) end end # # Processes each Subsection in this table. # def each_subsection(&b) @subsections.each(&b) end # # Returns an Array of Subsection. # def subsections @subsections end # # Clear all the entries. # def clear @subsections.clear end # # The number of XRef entries in the Section. # def size @subsections.reduce(0) { |total, subsection| total + subsection.size } end # # Outputs self into PDF code. # def to_s "xref" << EOL << @subsections.join end end end # # An xref poiting to an Object embedded in an ObjectStream. # class XRefToCompressedObj attr_accessor :objstmno, :index def initialize(objstmno, index) @objstmno = objstmno @index = index end def to_xrefstm_data(type_w, field1_w, field2_w) type_w <<= 3 field1_w <<= 3 field2_w <<= 3 type = "\002".unpack("B#{type_w}")[0] objstmno = @objstmno.to_s(2).rjust(field1_w, '0') index = @index.to_s(2).rjust(field2_w, '0') [ type , objstmno, index ].pack("B#{type_w}B#{field1_w}B#{field2_w}") end end class InvalidXRefStreamObjectError < InvalidStreamObjectError ; end # # Class representing a XRef Stream. # class XRefStream < Stream include Enumerable include StandardObject XREF_FREE = 0 XREF_USED = 1 XREF_COMPRESSED = 2 # # Xref fields # field :Type, :Type => Name, :Default => :XRef, :Required => true, :Version => "1.5" field :Size, :Type => Integer, :Required => true field :Index, :Type => Array.of(Integer, Integer) field :Prev, :Type => Integer field :W, :Type => Array.of(Integer, length: 3), :Required => true # # Trailer fields # field :Root, :Type => Catalog, :Required => true field :Encrypt, :Type => Encryption::Standard::Dictionary field :Info, :Type => Metadata field :ID, :Type => Array.of(String, length: 2) def initialize(data = "", dictionary = {}) super(data, dictionary) @xrefs = nil end def entries load! if @xrefs.nil? @xrefs end # # Returns XRef entries present in this stream. # def pre_build #:nodoc: load! if @xrefs.nil? self.W = [ 1, 2, 2 ] unless has_field?(:W) self.Size = @xrefs.length + 1 save! super end # # Adds an XRef to this Stream. # def <<(xref) load! if @xrefs.nil? @xrefs << xref end # # Iterates over each XRef present in the stream. # def each(&b) load! if @xrefs.nil? @xrefs.each(&b) end # # Iterates over each XRef present in the stream, passing the XRef and its object number. # def each_with_number return enum_for(__method__) unless block_given? load! if @xrefs.nil? ranges = object_ranges xrefs = @xrefs.to_enum ranges.each do |range| range.each do |no| begin yield(xrefs.next, no) rescue StopIteration raise InvalidXRefStreamObjectError, "Range is bigger than number of entries" end end end end # # Returns an XRef matching this object number. # def find(no) load! if @xrefs.nil? ranges = object_ranges index = 0 ranges.each do |range| return @xrefs[index + no - range.begin] if range.cover?(no) index += range.size end nil end def clear self.data = '' @xrefs = [] self.Index = [] end private def object_ranges load! if @xrefs.nil? if self.key?(:Index) ranges = self.Index unless ranges.is_a?(Array) and ranges.length.even? and ranges.all?{|i| i.is_a?(Integer)} raise InvalidXRefStreamObjectError, "Index must be an even Array of integers" end ranges.each_slice(2).map { |start, length| Range.new(start.to_i, start.to_i + length.to_i - 1) } else [ 0...@xrefs.size ] end end def load! #:nodoc: if @xrefs.nil? and has_field?(:W) widths = self.W if not widths.is_a?(Array) or widths.length != 3 or widths.any?{|width| not width.is_a?(Integer) } raise InvalidXRefStreamObjectError, "W field must be an array of 3 integers" end decode! type_w = self.W[0] field1_w = self.W[1] field2_w = self.W[2] entrymask = "B#{type_w << 3}B#{field1_w << 3}B#{field2_w << 3}" size = @data.size / (type_w + field1_w + field2_w) xentries = @data.unpack(entrymask * size).map!{|field| field.to_i(2) } @xrefs = [] size.times do |i| type,field1,field2 = xentries[i*3].ord,xentries[i*3+1].ord,xentries[i*3+2].ord case type when XREF_FREE @xrefs << XRef.new(field1, field2, XRef::FREE) when XREF_USED @xrefs << XRef.new(field1, field2, XRef::USED) when XREF_COMPRESSED @xrefs << XRefToCompressedObj.new(field1, field2) end end else @xrefs = [] end end def save! #:nodoc: self.data = "" type_w, field1_w, field2_w = self.W @xrefs.each do |xref| @data << xref.to_xrefstm_data(type_w, field1_w, field2_w) end encode! end end end origami-2.0.0/lib/origami/signature.rb0000644000004100000410000005466112757133666017747 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end begin require 'openssl' if Origami::OPTIONS[:use_openssl] rescue LoadError Origami::OPTIONS[:use_openssl] = false end require 'digest/sha1' module Origami class PDF class SignatureError < Error #:nodoc: end # # Verify a document signature. # _:trusted_certs_: an array of trusted X509 certificates. # If no argument is passed, embedded certificates are treated as trusted. # def verify(trusted_certs: []) unless Origami::OPTIONS[:use_openssl] fail "OpenSSL is not present or has been disabled." end digsig = self.signature unless digsig[:Contents].is_a?(String) raise SignatureError, "Invalid digital signature contents" end store = OpenSSL::X509::Store.new trusted_certs.each do |ca| store.add_cert(ca) end flags = 0 flags |= OpenSSL::PKCS7::NOVERIFY if trusted_certs.empty? stream = StringScanner.new(self.original_data) stream.pos = digsig[:Contents].file_offset Object.typeof(stream).parse(stream) endofsig_offset = stream.pos stream.terminate s1,l1,s2,l2 = digsig.ByteRange if s1.value != 0 or (s2.value + l2.value) != self.original_data.size or (s1.value + l1.value) != digsig[:Contents].file_offset or s2.value != endofsig_offset raise SignatureError, "Invalid signature byte range" end data = self.original_data[s1,l1] + self.original_data[s2,l2] case digsig.SubFilter.value.to_s when 'adbe.pkcs7.detached' flags |= OpenSSL::PKCS7::DETACHED p7 = OpenSSL::PKCS7.new(digsig[:Contents].value) raise SignatureError, "Not a PKCS7 detached signature" unless p7.detached? p7.verify([], store, data, flags) when 'adbe.pkcs7.sha1' p7 = OpenSSL::PKCS7.new(digsig[:Contents].value) p7.verify([], store, nil, flags) and p7.data == Digest::SHA1.digest(data) else raise NotImplementedError, "Unsupported method #{digsig.SubFilter}" end end # # Sign the document with the given key and x509 certificate. # _certificate_:: The X509 certificate containing the public key. # _key_:: The private key associated with the certificate. # _method_:: The PDF signature identifier. # _ca_:: Optional CA certificates used to sign the user certificate. # _annotation_:: Annotation associated with the signature. # _issuer_:: Issuer name. # _location_:: Signature location. # _contact_:: Signer contact. # _reason_:: Signing reason. # def sign(certificate, key, method: "adbe.pkcs7.detached", ca: [], annotation: nil, issuer: nil, location: nil, contact: nil, reason: nil) unless Origami::OPTIONS[:use_openssl] fail "OpenSSL is not present or has been disabled." end unless certificate.is_a?(OpenSSL::X509::Certificate) raise TypeError, "A OpenSSL::X509::Certificate object must be passed." end unless key.is_a?(OpenSSL::PKey::RSA) raise TypeError, "A OpenSSL::PKey::RSA object must be passed." end unless ca.is_a?(::Array) raise TypeError, "Expected an Array of CA certificate." end unless annotation.nil? or annotation.is_a?(Annotation::Widget::Signature) raise TypeError, "Expected a Annotation::Widget::Signature object." end case method when 'adbe.pkcs7.detached' signfield_size = -> (crt, pkey, certs) do OpenSSL::PKCS7.sign( crt, pkey, "", certs, OpenSSL::PKCS7::DETACHED | OpenSSL::PKCS7::BINARY ).to_der.size end when 'adbe.pkcs7.sha1' signfield_size = -> (crt, pkey, certs) do OpenSSL::PKCS7.sign( crt, pkey, Digest::SHA1.digest(''), certs, OpenSSL::PKCS7::BINARY ).to_der.size end when 'adbe.x509.rsa_sha1' signfield_size = -> (crt, pkey, certs) do pkey.private_encrypt( Digest::SHA1.digest('') ).size end raise NotImplementedError, "Unsupported method #{method.inspect}" else raise NotImplementedError, "Unsupported method #{method.inspect}" end digsig = Signature::DigitalSignature.new.set_indirect(true) if annotation.nil? annotation = Annotation::Widget::Signature.new annotation.Rect = Rectangle[:llx => 0.0, :lly => 0.0, :urx => 0.0, :ury => 0.0] end annotation.V = digsig add_fields(annotation) self.Catalog.AcroForm.SigFlags = InteractiveForm::SigFlags::SIGNATURESEXIST | InteractiveForm::SigFlags::APPENDONLY digsig.Type = :Sig #:nodoc: digsig.Contents = HexaString.new("\x00" * signfield_size[certificate, key, ca]) #:nodoc: digsig.Filter = :"Adobe.PPKLite" #:nodoc: digsig.SubFilter = Name.new(method) #:nodoc: digsig.ByteRange = [0, 0, 0, 0] #:nodoc: digsig.Name = issuer digsig.Location = HexaString.new(location) if location digsig.ContactInfo = HexaString.new(contact) if contact digsig.Reason = HexaString.new(reason) if reason if method == 'adbe.x509.rsa_sha1' digsig.Cert = if ca.empty? HexaString.new(certificate.to_der) else [ HexaString.new(certificate.to_der) ] + ca.map{ |crt| HexaString.new(crt.to_der) } end end # # Flattening the PDF to get file view. # compile # # Creating an empty Xref table to compute signature byte range. # rebuild_dummy_xrefs sig_offset = get_object_offset(digsig.no, digsig.generation) + digsig.signature_offset digsig.ByteRange[0] = 0 digsig.ByteRange[1] = sig_offset digsig.ByteRange[2] = sig_offset + digsig.Contents.to_s.bytesize until digsig.ByteRange[3] == filesize - digsig.ByteRange[2] digsig.ByteRange[3] = filesize - digsig.ByteRange[2] end # From that point on, the file size remains constant # # Correct Xrefs variations caused by ByteRange modifications. # rebuild_xrefs file_data = output() signable_data = file_data[digsig.ByteRange[0],digsig.ByteRange[1]] + file_data[digsig.ByteRange[2],digsig.ByteRange[3]] signature = case method when 'adbe.pkcs7.detached' OpenSSL::PKCS7.sign( certificate, key, signable_data, ca, OpenSSL::PKCS7::DETACHED | OpenSSL::PKCS7::BINARY ).to_der when 'adbe.pkcs7.sha1' OpenSSL::PKCS7.sign( certificate, key, Digest::SHA1.digest(signable_data), ca, OpenSSL::PKCS7::BINARY ).to_der when 'adbe.x509.rsa_sha1' key.private_encrypt(Digest::SHA1.digest(signable_data)) end digsig.Contents[0, signature.size] = signature # # No more modification are allowed after signing. # self.freeze end # # Returns whether the document contains a digital signature. # def signed? begin self.Catalog.AcroForm.is_a?(Dictionary) and self.Catalog.AcroForm.has_key?(:SigFlags) and (self.Catalog.AcroForm.SigFlags & InteractiveForm::SigFlags::SIGNATURESEXIST != 0) rescue InvalidReferenceError false end end # # Enable the document Usage Rights. # _rights_:: list of rights defined in UsageRights::Rights # def enable_usage_rights(cert, pkey, *rights) unless Origami::OPTIONS[:use_openssl] fail "OpenSSL is not present or has been disabled." end signfield_size = -> (crt, key, ca) do OpenSSL::PKCS7.sign( crt, key, '', ca, OpenSSL::PKCS7::DETACHED | OpenSSL::PKCS7::BINARY ).to_der.size end # # Load key pair # key = pkey.is_a?(OpenSSL::PKey::RSA) ? pkey : OpenSSL::PKey::RSA.new(pkey) certificate = cert.is_a?(OpenSSL::X509::Certificate) ? cert : OpenSSL::X509::Certificate.new(cert) # # Forge digital signature dictionary # digsig = Signature::DigitalSignature.new.set_indirect(true) self.Catalog.AcroForm ||= InteractiveForm.new #self.Catalog.AcroForm.SigFlags = InteractiveForm::SigFlags::APPENDONLY digsig.Type = :Sig #:nodoc: digsig.Contents = HexaString.new("\x00" * signfield_size[certificate, key, []]) #:nodoc: digsig.Filter = :"Adobe.PPKLite" #:nodoc: digsig.Name = "ARE Acrobat Product v8.0 P23 0002337" #:nodoc: digsig.SubFilter = :"adbe.pkcs7.detached" #:nodoc: digsig.ByteRange = [0, 0, 0, 0] #:nodoc: sigref = Signature::Reference.new #:nodoc: sigref.Type = :SigRef #:nodoc: sigref.TransformMethod = :UR3 #:nodoc: sigref.Data = self.Catalog sigref.TransformParams = UsageRights::TransformParams.new sigref.TransformParams.P = true #:nodoc: sigref.TransformParams.Type = :TransformParams #:nodoc: sigref.TransformParams.V = UsageRights::TransformParams::VERSION rights.each do |right| sigref.TransformParams[right.first] ||= [] sigref.TransformParams[right.first].concat(right[1..-1]) end digsig.Reference = [ sigref ] self.Catalog.Perms ||= Perms.new self.Catalog.Perms.UR3 = digsig # # Flattening the PDF to get file view. # compile # # Creating an empty Xref table to compute signature byte range. # rebuild_dummy_xrefs sig_offset = get_object_offset(digsig.no, digsig.generation) + digsig.signature_offset digsig.ByteRange[0] = 0 digsig.ByteRange[1] = sig_offset digsig.ByteRange[2] = sig_offset + digsig.Contents.size until digsig.ByteRange[3] == filesize - digsig.ByteRange[2] digsig.ByteRange[3] = filesize - digsig.ByteRange[2] end # From that point on, the file size remains constant # # Correct Xrefs variations caused by ByteRange modifications. # rebuild_xrefs file_data = output() signable_data = file_data[digsig.ByteRange[0],digsig.ByteRange[1]] + file_data[digsig.ByteRange[2],digsig.ByteRange[3]] signature = OpenSSL::PKCS7.sign( certificate, key, signable_data, [], OpenSSL::PKCS7::DETACHED | OpenSSL::PKCS7::BINARY ).to_der digsig.Contents[0, signature.size] = signature # # No more modification are allowed after signing. # self.freeze end def usage_rights? not self.Catalog.Perms.nil? and (not self.Catalog.Perms.has_key?(:UR3) or not self.Catalog.Perms.has_key?(:UR)) end def signature raise SignatureError, "Not a signed document" unless self.signed? self.each_field do |field| return field.V if field.FT == :Sig and field.V.is_a?(Dictionary) end raise SignatureError, "Cannot find digital signature" end end class Perms < Dictionary include StandardObject field :DocMDP, :Type => Dictionary field :UR, :Type => Dictionary field :UR3, :Type => Dictionary, :Version => "1.6" end module Signature # # Class representing a signature which can be embedded in DigitalSignature dictionary. # It must be a direct object. # class Reference < Dictionary include StandardObject field :Type, :Type => Name, :Default => :SigRef field :TransformMethod, :Type => Name, :Default => :DocMDP, :Required => true field :TransformParams, :Type => Dictionary field :Data, :Type => Object field :DigestMethod, :Type => Name, :Default => :MD5 field :DigestValue, :Type => String field :DigestLocation, :Type => Array def initialize(hash = {}, parser = nil) set_indirect(false) super(hash, parser) end end class BuildData < Dictionary include StandardObject field :Name, :Type => Name, :Version => "1.5" field :Date, :Type => String, :Version => "1.5" field :R, :Type => Number, :Version => "1.5" field :PreRelease, :Type => Boolean, :Default => false, :Version => "1.5" field :OS, :Type => Array, :Version => "1.5" field :NonEFontNoWarn, :Type => Boolean, :Version => "1.5" field :TrustedMode, :Type => Boolean, :Version => "1.5" field :V, :Type => Number, :Version => "1.5" def initialize(hash = {}, parser = nil) set_indirect(false) super(hash, parser) end end class AppData < BuildData field :REx, :Type => String, :Version => "1.6" end class SigQData < BuildData field :Preview, :Type => Boolean, :Default => false, :Version => "1.7" end class BuildProperties < Dictionary include StandardObject field :Filter, :Type => BuildData, :Version => "1.5" field :PubSec, :Type => BuildData, :Version => "1.5" field :App, :Type => AppData, :Version => "1.5" field :SigQ, :Type => SigQData, :Version => "1.7" def initialize(hash = {}, parser = nil) set_indirect(false) super(hash, parser) end def pre_build #:nodoc: self.Filter ||= BuildData.new self.Filter.Name ||= :"Adobe.PPKLite" self.Filter.R ||= 0x20020 self.Filter.V ||= 2 self.Filter.Date ||= Time.now.to_s self.PubSec ||= BuildData.new self.PubSec.NonEFontNoWarn ||= true self.PubSec.Date ||= Time.now.to_s self.PubSec.R ||= 0x20021 self.App ||= AppData.new self.App.Name ||= :Reader self.App.REx = "11.0.8" self.App.TrustedMode ||= true self.App.OS ||= [ :Win ] self.App.R ||= 0xb0008 super end end # # Class representing a digital signature. # class DigitalSignature < Dictionary include StandardObject field :Type, :Type => Name, :Default => :Sig field :Filter, :Type => Name, :Default => :"Adobe.PPKLite", :Required => true field :SubFilter, :Type => Name field :Contents, :Type => String, :Required => true field :Cert, :Type => [ String, Array.of(String) ] field :ByteRange, :Type => Array.of(Integer, length: 4) field :Reference, :Type => Array.of(Reference), :Version => "1.5" field :Changes, :Type => Array field :Name, :Type => String field :M, :Type => String field :Location, :Type => String field :Reason, :Type => String field :ContactInfo, :Type => String field :R, :Type => Integer field :V, :Type => Integer, :Default => 0, :Version => "1.5" field :Prop_Build, :Type => BuildProperties, :Version => "1.5" field :Prop_AuthTime, :Type => Integer, :Version => "1.5" field :Prop_AuthType, :Type => Name, :Version => "1.5" def pre_build #:nodoc: self.M = Origami::Date.now self.Prop_Build ||= BuildProperties.new.pre_build super end def to_s(indent: 1, tab: "\t") #:nodoc: # Must be deterministic. indent, tab = 1, "\t" content = TOKENS.first + EOL self.to_a.sort_by{ |key, _| key }.reverse.each do |key, value| content << tab * indent << key.to_s << " " content << (value.is_a?(Dictionary) ? value.to_s(indent: indent + 1) : value.to_s) << EOL end content << tab * (indent - 1) << TOKENS.last output(content) end def signature_offset #:nodoc: indent, tab = 1, "\t" content = "#{no} #{generation} obj" + EOL + TOKENS.first + EOL self.to_a.sort_by{ |key, _| key }.reverse.each do |key, value| if key == :Contents content << tab * indent + key.to_s + " " return content.size else content << tab * indent + key.to_s << " " content << (value.is_a?(Dictionary) ? value.to_s(indent: indent + 1) : value.to_s) << EOL end end nil end end end module UsageRights module Rights DOCUMENT_FULLSAVE = %i[Document FullSave] DOCUMENT_ALL = DOCUMENT_FULLSAVE ANNOTS_CREATE = %i[Annots Create] ANNOTS_DELETE = %i[Annots Delete] ANNOTS_MODIFY = %i[Annots Modify] ANNOTS_COPY = %i[Annots Copy] ANNOTS_IMPORT = %i[Annots Import] ANNOTS_EXPORT = %i[Annots Export] ANNOTS_ONLINE = %i[Annots Online] ANNOTS_SUMMARYVIEW = %i[Annots SummaryView] ANNOTS_ALL = %i[Annots Create Modify Copy Import Export Online SummaryView] FORM_FILLIN = %i[Form FillIn] FORM_IMPORT = %i[Form Import] FORM_EXPORT = %i[Form Export] FORM_SUBMITSTANDALONE = %i[Form SubmitStandAlone] FORM_SPAWNTEMPLATE = %i[Form SpawnTemplate] FORM_BARCODEPLAINTEXT = %i[Form BarcodePlaintext] FORM_ONLINE = %i[Form Online] FORM_ALL = %i[Form FillIn Import Export SubmitStandAlone SpawnTemplate BarcodePlaintext Online] FORMEX_BARCODEPLAINTEXT = %i[FormEx BarcodePlaintext] FORMEX_ALL = FORMEX_BARCODEPLAINTEXT SIGNATURE_MODIFY = %i[Signature Modify] SIGNATURE_ALL = SIGNATURE_MODIFY EF_CREATE = %i[EF Create] EF_DELETE = %i[EF Delete] EF_MODIFY = %i[EF Modify] EF_IMPORT = %i[EF Import] EF_ALL = %i[EF Create Delete Modify Import] ALL = [ DOCUMENT_ALL, ANNOTS_ALL, FORM_ALL, SIGNATURE_ALL, EF_ALL ] end class TransformParams < Dictionary include StandardObject VERSION = Name.new("2.2") field :Type, :Type => Name, :Default => :TransformParams field :Document, :Type => Array.of(Name) field :Msg, :Type => String field :V, :Type => Name, :Default => VERSION field :Annots, :Type => Array.of(Name) field :Form, :Type => Array.of(Name) field :FormEx, :Type => Array.of(Name) field :Signature, :Type => Array.of(Name) field :EF, :Type => Array.of(Name), :Version => "1.6" field :P, :Type => Boolean, :Default => false, :Version => "1.6" def initialize(hash = {}, parser = nil) set_indirect(false) super(hash, parser) end end end end origami-2.0.0/lib/origami/filters.rb0000644000004100000410000002471712757133666017415 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end module Origami # # Filters are algorithms used to encode data into a PDF Stream. # module Filter # # Base class for filter Exceptions. # class Error < Origami::Error attr_accessor :input_data, :decoded_data def initialize(message, input_data: nil, decoded_data: nil) super(message) @input_data, @decoded_data = input_data, decoded_data end end # # Exception class for unsupported filters or unsupported filter parameters. # class NotImplementedError < Error; end # # Exception class for errors occuring during decode operations. # class DecodeError < Error; end module Utils class BitWriterError < Error #:nodoc: end # # Class used to forge a String from a stream of bits. # Internally used by some filters. # class BitWriter def initialize @data = '' @last_byte = nil @ptr_bit = 0 end # # Writes _data_ represented as Fixnum to a _length_ number of bits. # def write(data, length) return BitWriterError, "Invalid data length" unless length > 0 and (1 << length) > data # optimization for aligned byte writing if length == 8 and @last_byte.nil? and @ptr_bit == 0 @data << data.chr return self end while length > 0 if length >= 8 - @ptr_bit length -= 8 - @ptr_bit @last_byte ||= 0 @last_byte |= (data >> length) & ((1 << (8 - @ptr_bit)) - 1) data &= (1 << length) - 1 @data << @last_byte.chr @last_byte = nil @ptr_bit = 0 else @last_byte ||= 0 @last_byte |= (data & ((1 << length) - 1)) << (8 - @ptr_bit - length) @ptr_bit += length if @ptr_bit == 8 @data << @last_byte.chr @last_byte = nil @ptr_bit = 0 end length = 0 end end self end # # Returns the data size in bits. # def size (@data.size << 3) + @ptr_bit end # # Finalizes the stream. # def final @data << @last_byte.chr if @last_byte @last_byte = nil @p = 0 self end # # Outputs the stream as a String. # def to_s @data.dup end end class BitReaderError < Error #:nodoc: end # # Class used to read a String as a stream of bits. # Internally used by some filters. # class BitReader BRUIJIN_TABLE = ::Array.new(32) BRUIJIN_TABLE.size.times do |i| BRUIJIN_TABLE[((0x77cb531 * (1 << i)) >> 27) & 31] = i end def initialize(data) @data = data reset end # # Resets the read pointer. # def reset @ptr_byte, @ptr_bit = 0, 0 self end # # Returns true if end of data has been reached. # def eod? @ptr_byte >= @data.size end # # Returns the read pointer position in bits. # def pos (@ptr_byte << 3) + @ptr_bit end # # Returns the data size in bits. # def size @data.size << 3 end # # Sets the read pointer position in bits. # def pos=(bits) raise BitReaderError, "Pointer position out of data" if bits > self.size pbyte = bits >> 3 pbit = bits - (pbyte << 3) @ptr_byte, @ptr_bit = pbyte, pbit bits end # # Reads _length_ bits as a Fixnum and advances read pointer. # def read(length) n = self.peek(length) self.pos += length n end # # Reads _length_ bits as a Fixnum. Does not advance read pointer. # def peek(length) return BitReaderError, "Invalid read length" unless length > 0 return BitReaderError, "Insufficient data" if self.pos + length > self.size n = 0 ptr_byte, ptr_bit = @ptr_byte, @ptr_bit while length > 0 byte = @data[ptr_byte].ord if length > 8 - ptr_bit length -= 8 - ptr_bit n |= ( byte & ((1 << (8 - ptr_bit)) - 1) ) << length ptr_byte += 1 ptr_bit = 0 else n |= (byte >> (8 - ptr_bit - length)) & ((1 << length) - 1) length = 0 end end n end # # Used for bit scanning. # Counts leading zeros. Does not advance read pointer. # def clz count = 0 if @ptr_bit != 0 bits = peek(8 - @ptr_bit) count = clz32(bits << (32 - (8 - @ptr_bit))) return count if count < (8 - @ptr_bit) end delta = 0 while @data.size > @ptr_byte + delta * 4 word = @data[@ptr_byte + delta * 4, 4] # next 32 bits z = clz32((word << (4 - word.size)).unpack("N")[0]) count += z delta += 1 return count if z < 32 - ((4 - word.size) << 3) end count end # # Used for bit scanning. # Count leading ones. Does not advance read pointer. # def clo count = 0 if @ptr_bit != 0 bits = peek(8 - @ptr_bit) count = clz32(~(bits << (32 - (8 - @ptr_bit))) & 0xff) return count if count < (8 - @ptr_bit) end delta = 0 while @data.size > @ptr_byte + delta * 4 word = @data[@ptr_byte + delta * 4, 4] # next 32 bits z = clz32(~((word << (4 - word.size)).unpack("N")[0]) & 0xffff_ffff) count += z delta += 1 return count if z < 32 - ((4 - word.size) << 3) end count end private def bitswap8(i) #:nodoc ((i * 0x0202020202) & 0x010884422010) % 1023 end def bitswap32(i) #:nodoc: (bitswap8((i >> 0) & 0xff) << 24) | (bitswap8((i >> 8) & 0xff) << 16) | (bitswap8((i >> 16) & 0xff) << 8) | (bitswap8((i >> 24) & 0xff) << 0) end def ctz32(i) #:nodoc: if i == 0 then 32 else BRUIJIN_TABLE[(((i & -i) * 0x77cb531) >> 27) & 31] end end def clz32(i) #:nodoc: ctz32 bitswap32 i end end end module ClassMethods # # Decodes the given data. # _stream_:: The data to decode. # def decode(stream, params = {}) self.new(params).decode(stream) end # # Encodes the given data. # _stream_:: The data to encode. # def encode(stream, params = {}) self.new(params).encode(stream) end end def initialize(parameters = {}) @params = parameters end def self.included(receiver) receiver.extend(ClassMethods) end end end require 'origami/filters/ascii' require 'origami/filters/lzw' require 'origami/filters/flate' require 'origami/filters/runlength' require 'origami/filters/ccitt' require 'origami/filters/dct' require 'origami/filters/jbig2' require 'origami/filters/jpx' require 'origami/filters/crypt' origami-2.0.0/lib/origami/reference.rb0000644000004100000410000000576412757133666017704 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end module Origami class InvalidReferenceError < Error #:nodoc: end # # Class representing a Reference Object. # Reference are like symbolic links pointing to a particular object into the file. # class Reference include Origami::Object TOKENS = [ "(?\\d+)" + WHITESPACES + "(?\\d+)" + WHITESPACES + "R" ] #:nodoc: REGEXP_TOKEN = Regexp.new(TOKENS.first, Regexp::MULTILINE) @@regexp = Regexp.new(WHITESPACES + TOKENS.first + WHITESPACES) attr_accessor :refno, :refgen def initialize(refno, refgen) super() @refno, @refgen = refno, refgen end def self.parse(stream, parser = nil) #:nodoc: offset = stream.pos if stream.scan(@@regexp).nil? raise InvalidReferenceError, "Bad reference to indirect objet format" end no = stream['no'].to_i gen = stream['gen'].to_i ref = Reference.new(no, gen) ref.file_offset = offset ref end def solve doc = self.document if doc.nil? raise InvalidReferenceError, "Not attached to any document" end target = doc.get_object(self) if target.nil? and not Origami::OPTIONS[:ignore_bad_references] raise InvalidReferenceError, "Cannot resolve reference : #{self.to_s}" end target or Null.new end def hash #:nodoc: self.to_a.hash end def <=>(ref) #:nodoc self.to_a <=> ref.to_a end # # Compares to Reference object. # def ==(ref) return false unless ref.is_a?(Reference) self.to_a == ref.to_a end alias eql? == # # Returns a Ruby array with the object number and the generation this reference is pointing to. # def to_a [@refno, @refgen] end def to_s #:nodoc: super("#{@refno} #{@refgen} R") end # # Returns self. # def value self end def self.native_type ; Reference end end end origami-2.0.0/lib/origami/encryption.rb0000644000004100000410000015166512757133666020142 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end begin require 'openssl' if Origami::OPTIONS[:use_openssl] rescue LoadError Origami::OPTIONS[:use_openssl] = false end require 'securerandom' require 'digest/md5' require 'digest/sha2' module Origami class EncryptionError < Error #:nodoc: end class EncryptionInvalidPasswordError < EncryptionError #:nodoc: end class EncryptionNotSupportedError < EncryptionError #:nodoc: end class PDF # # Returns whether the PDF file is encrypted. # def encrypted? trailer_key? :Encrypt end # # Decrypts the current document (only RC4 40..128 bits). # _passwd_:: The password to decrypt the document. # def decrypt(passwd = "") raise EncryptionError, "PDF is not encrypted" unless self.encrypted? encrypt_dict = trailer_key(:Encrypt) handler = Encryption::Standard::Dictionary.new(encrypt_dict.dup) unless handler.Filter == :Standard raise EncryptionNotSupportedError, "Unknown security handler : '#{handler.Filter.to_s}'" end crypt_filters = { Identity: Encryption::Identity } case handler.V.to_i when 1,2 crypt_filters = Hash.new(Encryption::RC4) string_filter = stream_filter = nil when 4,5 crypt_filters = { Identity: Encryption::Identity } if handler[:CF].is_a?(Dictionary) handler[:CF].each_pair do |name, cf| next unless cf.is_a?(Dictionary) crypt_filters[name.value] = if cf[:CFM] == :V2 then Encryption::RC4 elsif cf[:CFM] == :AESV2 then Encryption::AES elsif cf[:CFM] == :None then Encryption::Identity elsif cf[:CFM] == :AESV3 and handler.V.to_i == 5 then Encryption::AES else raise EncryptionNotSupportedError, "Unsupported encryption version : #{handler.V}" end end end string_filter = handler.StrF.is_a?(Name) ? handler.StrF.value : :Identity stream_filter = handler.StmF.is_a?(Name) ? handler.StmF.value : :Identity unless crypt_filters.key?(string_filter) raise EncryptionError, "Invalid StrF value in encryption dictionary" end unless crypt_filters.key?(stream_filter) raise EncryptionError, "Invalid StmF value in encryption dictionary" end else raise EncryptionNotSupportedError, "Unsupported encryption version : #{handler.V}" end doc_id = trailer_key(:ID) unless doc_id.is_a?(Array) raise EncryptionError, "Document ID was not found or is invalid" unless handler.V.to_i == 5 else doc_id = doc_id.first end if handler.is_user_password?(passwd, doc_id) encryption_key = handler.compute_user_encryption_key(passwd, doc_id) elsif handler.is_owner_password?(passwd, doc_id) if handler.V.to_i < 5 user_passwd = handler.retrieve_user_password(passwd) encryption_key = handler.compute_user_encryption_key(user_passwd, doc_id) else encryption_key = handler.compute_owner_encryption_key(passwd) end else raise EncryptionInvalidPasswordError end encrypt_metadata = (handler.EncryptMetadata != false) self.extend(Encryption::EncryptedDocument) self.encryption_handler = handler self.crypt_filters = crypt_filters self.encryption_key = encryption_key self.stm_filter, self.str_filter = stream_filter, string_filter # # Should be fixed to exclude only the active XRefStream # metadata = self.Catalog.Metadata self.indirect_objects.each do |indobj| encrypted_objects = [] case indobj when String,Stream then encrypted_objects << indobj when Dictionary,Array then encrypted_objects |= indobj.strings_cache end encrypted_objects.each do |obj| case obj when String next if obj.equal?(encrypt_dict[:U]) or obj.equal?(encrypt_dict[:O]) or obj.equal?(encrypt_dict[:UE]) or obj.equal?(encrypt_dict[:OE]) or obj.equal?(encrypt_dict[:Perms]) or (obj.parent.is_a?(Signature::DigitalSignature) and obj.equal?(obj.parent[:Contents])) obj.extend(Encryption::EncryptedString) unless obj.is_a?(Encryption::EncryptedString) obj.decrypt! when Stream next if obj.is_a?(XRefStream) or (not encrypt_metadata and obj.equal?(metadata)) obj.extend(Encryption::EncryptedStream) unless obj.is_a?(Encryption::EncryptedStream) end end end self end # # Encrypts the current document with the provided passwords. # The document will be encrypted at writing-on-disk time. # _userpasswd_:: The user password. # _ownerpasswd_:: The owner password. # _options_:: A set of options to configure encryption. # def encrypt(options = {}) raise EncryptionError, "PDF is already encrypted" if self.encrypted? # # Default encryption options. # params = { :user_passwd => '', :owner_passwd => '', :cipher => 'rc4', # :RC4 or :AES :key_size => 128, # Key size in bits :hardened => false, # Use newer password validation (since Reader X) :encrypt_metadata => true, # Metadata shall be encrypted? :permissions => Encryption::Standard::Permissions::ALL # Document permissions }.update(options) userpasswd, ownerpasswd = params[:user_passwd], params[:owner_passwd] case params[:cipher].upcase when 'RC4' algorithm = Encryption::RC4 if (40..128) === params[:key_size] and params[:key_size] % 8 == 0 if params[:key_size] > 40 version = 2 revision = 3 else version = 1 revision = 2 end else raise EncryptionError, "Invalid RC4 key length" end crypt_filters = Hash.new(algorithm) string_filter = stream_filter = nil when 'AES' algorithm = Encryption::AES if params[:key_size] == 128 version = revision = 4 elsif params[:key_size] == 256 version = 5 if params[:hardened] revision = 6 else revision = 5 end else raise EncryptionError, "Invalid AES key length (Only 128 and 256 bits keys are supported)" end crypt_filters = { Identity: Encryption::Identity, StdCF: algorithm } string_filter = stream_filter = :StdCF else raise EncryptionNotSupportedError, "Cipher not supported : #{params[:cipher]}" end doc_id = (trailer_key(:ID) || generate_id).first handler = Encryption::Standard::Dictionary.new handler.Filter = :Standard #:nodoc: handler.V = version handler.R = revision handler.Length = params[:key_size] handler.P = -1 # params[:Permissions] if revision >= 4 handler.EncryptMetadata = params[:encrypt_metadata] handler.CF = Dictionary.new cryptfilter = Encryption::CryptFilterDictionary.new cryptfilter.AuthEvent = :DocOpen if revision == 4 cryptfilter.CFM = :AESV2 else cryptfilter.CFM = :AESV3 end cryptfilter.Length = params[:key_size] >> 3 handler.CF[:StdCF] = cryptfilter handler.StmF = handler.StrF = :StdCF end handler.set_passwords(ownerpasswd, userpasswd, doc_id) encryption_key = handler.compute_user_encryption_key(userpasswd, doc_id) file_info = get_trailer_info file_info[:Encrypt] = self << handler self.extend(Encryption::EncryptedDocument) self.encryption_handler = handler self.encryption_key = encryption_key self.crypt_filters = crypt_filters self.stm_filter = self.str_filter = :StdCF self end end # # Module to provide support for encrypting and decrypting PDF documents. # module Encryption # # Generates _n_ random bytes from a fast PRNG. # def self.rand_bytes(n) Random.new.bytes(n) end # # Generates _n_ random bytes from a crypto PRNG. # def self.strong_rand_bytes(n) if Origami::OPTIONS[:use_openssl] OpenSSL::Random.random_bytes(n) else SecureRandom.random_bytes(n) end end module EncryptedDocument attr_accessor :encryption_key attr_accessor :encryption_handler attr_accessor :str_filter, :stm_filter attr_accessor :crypt_filters # Get the encryption cipher from the crypt filter name. def encryption_cipher(name) @crypt_filters[name] end # Get the default string encryption cipher. def string_encryption_cipher encryption_cipher @str_filter end # Get the default stream encryption cipher. def stream_encryption_cipher encryption_cipher @stm_filter end private def physicalize(options = {}) build = -> (obj, revision) do if obj.is_a?(EncryptedObject) if options[:decrypt] == true obj.pre_build obj.decrypt! obj.decrypted = false # makes it believe no encryption pass is required obj.post_build return end end if obj.is_a?(ObjectStream) obj.each do |subobj| build.call(subobj, revision) end end obj.pre_build case obj when String if not obj.equal?(@encryption_handler[:U]) and not obj.equal?(@encryption_handler[:O]) and not obj.equal?(@encryption_handler[:UE]) and not obj.equal?(@encryption_handler[:OE]) and not obj.equal?(@encryption_handler[:Perms]) and not (obj.parent.is_a?(Signature::DigitalSignature) and obj.equal?(obj.parent[:Contents])) and not obj.indirect_parent.parent.is_a?(ObjectStream) unless obj.is_a?(EncryptedString) obj.extend(EncryptedString) obj.decrypted = true end end when Stream return if obj.is_a?(XRefStream) return if obj.equal?(self.Catalog.Metadata) and not @encryption_handler.EncryptMetadata unless obj.is_a?(EncryptedStream) obj.extend(EncryptedStream) obj.decrypted = true end when Dictionary, Array obj.map! do |subobj| if subobj.indirect? if get_object(subobj.reference) subobj.reference else ref = add_to_revision(subobj, revision) build.call(subobj, revision) ref end else subobj end end obj.each do |subobj| build.call(subobj, revision) end end obj.post_build end # stack up every root objects indirect_objects_by_rev.each do |obj, revision| build.call(obj, revision) end # remove encrypt dictionary if requested if options[:decrypt] delete_object(get_trailer_info[:Encrypt]) get_trailer_info[:Encrypt] = nil end self end end # # Module for encrypted PDF objects. # module EncryptedObject #:nodoc attr_accessor :decrypted def post_build encrypt! super end private def compute_object_key(cipher) doc = self.document raise EncryptionError, "Document is not encrypted" unless doc.is_a?(EncryptedDocument) encryption_key = doc.encryption_key if doc.encryption_handler.V < 5 parent = self.indirect_parent no, gen = parent.no, parent.generation k = encryption_key + [no].pack("I")[0..2] + [gen].pack("I")[0..1] key_len = (k.length > 16) ? 16 : k.length k << "sAlT" if cipher == Encryption::AES Digest::MD5.digest(k)[0, key_len] else encryption_key end end end # # Module for encrypted String. # module EncryptedString include EncryptedObject def self.extended(obj) obj.decrypted = false end def encrypt! return self unless @decrypted cipher = self.document.string_encryption_cipher raise EncryptionError, "Cannot find string encryption filter" if cipher.nil? key = compute_object_key(cipher) encrypted_data = if cipher == RC4 or cipher == Identity cipher.encrypt(key, self.value) else iv = Encryption.rand_bytes(AES::BLOCKSIZE) cipher.encrypt(key, iv, self.value) end @decrypted = false self.replace(encrypted_data) self.freeze self end def decrypt! return self if @decrypted cipher = self.document.string_encryption_cipher raise EncryptionError, "Cannot find string encryption filter" if cipher.nil? key = compute_object_key(cipher) self.replace(cipher.decrypt(key, self.to_str)) @decrypted = true self end end # # Module for encrypted Stream. # module EncryptedStream include EncryptedObject def self.extended(obj) obj.decrypted = false end def encrypt! return self unless @decrypted encode! if self.filters.first == :Crypt params = decode_params.first if params.is_a?(Dictionary) and params.Name.is_a?(Name) crypt_filter = params.Name.value else crypt_filter = :Identity end cipher = self.document.encryption_cipher(crypt_filter) else cipher = self.document.stream_encryption_cipher end raise EncryptionError, "Cannot find stream encryption filter" if cipher.nil? key = compute_object_key(cipher) @encoded_data = if cipher == RC4 or cipher == Identity cipher.encrypt(key, self.encoded_data) else iv = Encryption.rand_bytes(AES::BLOCKSIZE) cipher.encrypt(key, iv, @encoded_data) end @decrypted = false @encoded_data.freeze self.freeze self end def decrypt! return self if @decrypted if self.filters.first == :Crypt params = decode_params.first if params.is_a?(Dictionary) and params.Name.is_a?(Name) crypt_filter = params.Name.value else crypt_filter = :Identity end cipher = self.document.encryption_cipher(crypt_filter) else cipher = self.document.stream_encryption_cipher end raise EncryptionError, "Cannot find stream encryption filter" if cipher.nil? key = compute_object_key(cipher) self.encoded_data = cipher.decrypt(key, @encoded_data) @decrypted = true self end end # # Identity transformation. # module Identity def Identity.encrypt(key, data) data end def Identity.decrypt(key, data) data end end # # Pure Ruby implementation of the RC4 symmetric algorithm # class RC4 # # Encrypts data using the given key # def RC4.encrypt(key, data) RC4.new(key).encrypt(data) end # # Decrypts data using the given key # def RC4.decrypt(key, data) RC4.new(key).decrypt(data) end # # Creates and initialises a new RC4 generator using given key # def initialize(key) if Origami::OPTIONS[:use_openssl] @key = key else @state = init(key) end end # # Encrypt/decrypt data with the RC4 encryption algorithm # def cipher(data) return "" if data.empty? if Origami::OPTIONS[:use_openssl] rc4 = OpenSSL::Cipher::RC4.new.encrypt rc4.key_len = @key.length rc4.key = @key output = rc4.update(data) << rc4.final else output = "" i, j = 0, 0 data.each_byte do |byte| i = i.succ & 0xFF j = (j + @state[i]) & 0xFF @state[i], @state[j] = @state[j], @state[i] output << (@state[@state[i] + @state[j] & 0xFF] ^ byte).chr end end output end alias encrypt cipher alias decrypt cipher private def init(key) #:nodoc: state = (0..255).to_a j = 0 256.times do |i| j = ( j + state[i] + key[i % key.size].ord ) & 0xFF state[i], state[j] = state[j], state[i] end state end end # # Pure Ruby implementation of the AES symmetric algorithm. # Using mode CBC. # class AES NROWS = 4 NCOLS = 4 BLOCKSIZE = NROWS * NCOLS ROUNDS = { 16 => 10, 24 => 12, 32 => 14 } # # Rijndael S-box # SBOX = [ 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16 ] # # Inverse of the Rijndael S-box # RSBOX = [ 0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb, 0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb, 0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e, 0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 0x6d, 0x8b, 0xd1, 0x25, 0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92, 0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84, 0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 0xb8, 0xb3, 0x45, 0x06, 0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b, 0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73, 0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 0x1c, 0x75, 0xdf, 0x6e, 0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b, 0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4, 0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xec, 0x5f, 0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef, 0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61, 0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d ] RCON = [ 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb ] attr_writer :iv def AES.encrypt(key, iv, data) AES.new(key, iv).encrypt(data) end def AES.decrypt(key, data) AES.new(key, nil).decrypt(data) end def initialize(key, iv, use_padding = true) unless key.size == 16 or key.size == 24 or key.size == 32 raise EncryptionError, "Key must have a length of 128, 192 or 256 bits." end if not iv.nil? and iv.size != BLOCKSIZE raise EncryptionError, "Initialization vector must have a length of #{BLOCKSIZE} bytes." end @key = key @iv = iv @use_padding = use_padding end def encrypt(data) if @iv.nil? raise EncryptionError, "No initialization vector has been set." end if @use_padding padlen = BLOCKSIZE - (data.size % BLOCKSIZE) data << (padlen.chr * padlen) end if Origami::OPTIONS[:use_openssl] aes = OpenSSL::Cipher::Cipher.new("aes-#{@key.length << 3}-cbc").encrypt aes.iv = @iv aes.key = @key aes.padding = 0 @iv + aes.update(data) + aes.final else cipher = [] cipherblock = [] nblocks = data.size / BLOCKSIZE first_round = true nblocks.times do |n| plainblock = data[n * BLOCKSIZE, BLOCKSIZE].unpack("C*") if first_round BLOCKSIZE.times do |i| plainblock[i] ^= @iv[i].ord end else BLOCKSIZE.times do |i| plainblock[i] ^= cipherblock[i] end end first_round = false cipherblock = aes_encrypt(plainblock) cipher.concat(cipherblock) end @iv + cipher.pack("C*") end end def decrypt(data) unless data.size % BLOCKSIZE == 0 raise EncryptionError, "Data must be 16-bytes padded (data size = #{data.size} bytes)" end @iv = data.slice!(0, BLOCKSIZE) if Origami::OPTIONS[:use_openssl] aes = OpenSSL::Cipher::Cipher.new("aes-#{@key.length << 3}-cbc").decrypt aes.iv = @iv aes.key = @key aes.padding = 0 plain = (aes.update(data) + aes.final).unpack("C*") else plain = [] plainblock = [] prev_cipherblock = [] nblocks = data.size / BLOCKSIZE first_round = true nblocks.times do |n| cipherblock = data[n * BLOCKSIZE, BLOCKSIZE].unpack("C*") plainblock = aes_decrypt(cipherblock) if first_round BLOCKSIZE.times do |i| plainblock[i] ^= @iv[i].ord end else BLOCKSIZE.times do |i| plainblock[i] ^= prev_cipherblock[i] end end first_round = false prev_cipherblock = cipherblock plain.concat(plainblock) end end if @use_padding padlen = plain[-1] unless (1..16) === padlen raise EncryptionError, "Incorrect padding length : #{padlen}" end padlen.times do pad = plain.pop raise EncryptionError, "Incorrect padding byte : 0x#{pad.to_s 16}" if pad != padlen end end plain.pack("C*") end private def rol(row, n = 1) #:nodoc n.times do row.push row.shift end ; row end def ror(row, n = 1) #:nodoc: n.times do row.unshift row.pop end ; row end def galois_mult(a, b) #:nodoc: p = 0 8.times do p ^= a if b[0] == 1 highBit = a[7] a <<= 1 a ^= 0x1b if highBit == 1 b >>= 1 end p % 256 end def schedule_core(word, iter) #:nodoc: rol(word) word.map! do |byte| SBOX[byte] end word[0] ^= RCON[iter] word end def transpose(m) #:nodoc: [ m[NROWS * 0, NROWS], m[NROWS * 1, NROWS], m[NROWS * 2, NROWS], m[NROWS * 3, NROWS] ].transpose.flatten end # # AES round methods. # def create_round_key(expanded_key, round = 0) #:nodoc: transpose(expanded_key[round * BLOCKSIZE, BLOCKSIZE]) end def add_round_key(roundKey) #:nodoc: BLOCKSIZE.times do |i| @state[i] ^= roundKey[i] end end def sub_bytes #:nodoc: BLOCKSIZE.times do |i| @state[i] = SBOX[ @state[i] ] end end def r_sub_bytes #:nodoc: BLOCKSIZE.times do |i| @state[i] = RSBOX[ @state[i] ] end end def shift_rows #:nodoc: NROWS.times do |i| @state[i * NCOLS, NCOLS] = rol(@state[i * NCOLS, NCOLS], i) end end def r_shift_rows #:nodoc: NROWS.times do |i| @state[i * NCOLS, NCOLS] = ror(@state[i * NCOLS, NCOLS], i) end end def mix_column_with_field(column, field) #:nodoc: p = field column[0], column[1], column[2], column[3] = galois_mult(column[0], p[0]) ^ galois_mult(column[3], p[1]) ^ galois_mult(column[2], p[2]) ^ galois_mult(column[1], p[3]), galois_mult(column[1], p[0]) ^ galois_mult(column[0], p[1]) ^ galois_mult(column[3], p[2]) ^ galois_mult(column[2], p[3]), galois_mult(column[2], p[0]) ^ galois_mult(column[1], p[1]) ^ galois_mult(column[0], p[2]) ^ galois_mult(column[3], p[3]), galois_mult(column[3], p[0]) ^ galois_mult(column[2], p[1]) ^ galois_mult(column[1], p[2]) ^ galois_mult(column[0], p[3]) end def mix_column(column) #:nodoc: mix_column_with_field(column, [ 2, 1, 1, 3 ]) end def r_mix_column_(column) #:nodoc: mix_column_with_field(column, [ 14, 9, 13, 11 ]) end def mix_columns #:nodoc: NCOLS.times do |c| column = [] NROWS.times do |r| column << @state[c + r * NCOLS] end mix_column(column) NROWS.times do |r| @state[c + r * NCOLS] = column[r] end end end def r_mix_columns #:nodoc: NCOLS.times do |c| column = [] NROWS.times do |r| column << @state[c + r * NCOLS] end r_mix_column_(column) NROWS.times do |r| @state[c + r * NCOLS] = column[r] end end end def expand_key(key) #:nodoc: key = key.unpack("C*") size = key.size expanded_size = 16 * (ROUNDS[key.size] + 1) rcon_iter = 1 expanded_key = key[0, size] while expanded_key.size < expanded_size temp = expanded_key[-4, 4] if expanded_key.size % size == 0 schedule_core(temp, rcon_iter) rcon_iter = rcon_iter.succ end temp.map! do |b| SBOX[b] end if size == 32 and expanded_key.size % size == 16 temp.each do |b| expanded_key << (expanded_key[-size] ^ b) end end expanded_key end def aes_round(round_key) #:nodoc: sub_bytes #puts "after sub_bytes: #{@state.inspect}" shift_rows #puts "after shift_rows: #{@state.inspect}" mix_columns #puts "after mix_columns: #{@state.inspect}" add_round_key(round_key) #puts "roundKey = #{roundKey.inspect}" #puts "after add_round_key: #{@state.inspect}" end def r_aes_round(round_key) #:nodoc: add_round_key(round_key) r_mix_columns r_shift_rows r_sub_bytes end def aes_encrypt(block) #:nodoc: @state = transpose(block) expanded_key = expand_key(@key) rounds = ROUNDS[@key.size] aes_main(expanded_key, rounds) end def aes_decrypt(block) #:nodoc: @state = transpose(block) expanded_key = expand_key(@key) rounds = ROUNDS[@key.size] r_aes_main(expanded_key, rounds) end def aes_main(expanded_key, rounds) #:nodoc: #puts "expandedKey: #{expandedKey.inspect}" round_key = create_round_key(expanded_key) add_round_key(round_key) for i in 1..rounds-1 round_key = create_round_key(expanded_key, i) aes_round(round_key) end round_key = create_round_key(expanded_key, rounds) sub_bytes shift_rows add_round_key(round_key) transpose(@state) end def r_aes_main(expanded_key, rounds) #:nodoc: round_key = create_round_key(expanded_key, rounds) add_round_key(round_key) r_shift_rows r_sub_bytes (rounds - 1).downto(1) do |i| round_key = create_round_key(expanded_key, i) r_aes_round(round_key) end round_key = create_round_key(expanded_key) add_round_key(round_key) transpose(@state) end end # # Class representing a crypt filter Dictionary # class CryptFilterDictionary < Dictionary include StandardObject field :Type, :Type => Name, :Default => :CryptFilter field :CFM, :Type => Name, :Default => :None field :AuthEvent, :Type => Name, :Default => :DocOpen field :Length, :Type => Integer end # # Common class for encryption dictionaries. # class EncryptionDictionary < Dictionary include StandardObject field :Filter, :Type => Name, :Default => :Standard, :Required => true field :SubFilter, :Type => Name, :Version => "1.3" field :V, :Type => Integer, :Default => 0 field :Length, :Type => Integer, :Default => 40, :Version => "1.4" field :CF, :Type => Dictionary, :Version => "1.5" field :StmF, :Type => Name, :Default => :Identity, :Version => "1.5" field :StrF, :Type => Name, :Default => :Identity, :Version => "1.5" field :EFF, :Type => Name, :Version => "1.6" end # # The standard security handler for PDF encryption. # module Standard PADDING = "\x28\xBF\x4E\x5E\x4E\x75\x8A\x41\x64\x00\x4E\x56\xFF\xFA\x01\x08\x2E\x2E\x00\xB6\xD0\x68\x3E\x80\x2F\x0C\xA9\xFE\x64\x53\x69\x7A".b #:nodoc: # # Permission constants for encrypted documents. # module Permissions RESERVED = 1 << 6 | 1 << 7 | 0xFFFFF000 PRINT = 1 << 2 | RESERVED MODIFY_CONTENTS = 1 << 3 | RESERVED COPY_CONTENTS = 1 << 4 | RESERVED MODIFY_ANNOTATIONS = 1 << 5 | RESERVED FILLIN_FORMS = 1 << 8 | RESERVED EXTRACT_CONTENTS = 1 << 9 | RESERVED ASSEMBLE_DOC = 1 << 10 | RESERVED HIGH_QUALITY_PRINT = 1 << 11 | RESERVED ALL = PRINT | MODIFY_CONTENTS | COPY_CONTENTS | MODIFY_ANNOTATIONS | FILLIN_FORMS | EXTRACT_CONTENTS | ASSEMBLE_DOC | HIGH_QUALITY_PRINT end # # Class defining a standard encryption dictionary. # class Dictionary < EncryptionDictionary field :R, :Type => Number, :Required => true field :O, :Type => String, :Required => true field :U, :Type => String, :Required => true field :OE, :Type => String, :Version => '1.7', :ExtensionLevel => 3 field :UE, :Type => String, :Version => '1.7', :ExtensionLevel => 3 field :Perms, :Type => String, :Version => '1.7', :ExtensionLevel => 3 field :P, :Type => Integer, :Default => 0, :Required => true field :EncryptMetadata, :Type => Boolean, :Default => true, :Version => "1.5" def version_required #:nodoc: if self.R > 5 [ 1.7, 8 ] else super end end # # Computes the key that will be used to encrypt/decrypt the document contents with user password. # def compute_user_encryption_key(userpassword, fileid) if self.R < 5 padded = pad_password(userpassword) padded.force_encoding('binary') padded << self.O padded << [ self.P ].pack("i") padded << fileid encrypt_metadata = self.EncryptMetadata != false padded << [ -1 ].pack("i") if self.R >= 4 and not encrypt_metadata key = Digest::MD5.digest(padded) 50.times { key = Digest::MD5.digest(key[0, self.Length / 8]) } if self.R >= 3 if self.R == 2 key[0, 5] elsif self.R >= 3 key[0, self.Length / 8] end else passwd = password_to_utf8(userpassword) uks = self.U[40, 8] if self.R == 5 ukey = Digest::SHA256.digest(passwd + uks) else ukey = compute_hardened_hash(passwd, uks) end iv = ::Array.new(AES::BLOCKSIZE, 0).pack("C*") AES.new(ukey, nil, false).decrypt(iv + self.UE.value) end end # # Computes the key that will be used to encrypt/decrypt the document contents with owner password. # Revision 5 and above. # def compute_owner_encryption_key(ownerpassword) if self.R >= 5 passwd = password_to_utf8(ownerpassword) oks = self.O[40, 8] if self.R == 5 okey = Digest::SHA256.digest(passwd + oks + self.U) else okey = compute_hardened_hash(passwd, oks, self.U) end iv = ::Array.new(AES::BLOCKSIZE, 0).pack("C*") AES.new(okey, nil, false).decrypt(iv + self.OE.value) end end # # Set up document passwords. # def set_passwords(ownerpassword, userpassword, salt = nil) if self.R < 5 key = compute_owner_key(ownerpassword) upadded = pad_password(userpassword) owner_key = RC4.encrypt(key, upadded) 19.times { |i| owner_key = RC4.encrypt(xor(key,i+1), owner_key) } if self.R >= 3 self.O = owner_key self.U = compute_user_password(userpassword, salt) else upass = password_to_utf8(userpassword) opass = password_to_utf8(ownerpassword) uvs, uks, ovs, oks = ::Array.new(4) { Encryption.rand_bytes(8) } file_key = Encryption.strong_rand_bytes(32) iv = ::Array.new(AES::BLOCKSIZE, 0).pack("C*") if self.R == 5 self.U = Digest::SHA256.digest(upass + uvs) + uvs + uks self.O = Digest::SHA256.digest(opass + ovs + self.U) + ovs + oks ukey = Digest::SHA256.digest(upass + uks) okey = Digest::SHA256.digest(opass + oks + self.U) else self.U = compute_hardened_hash(upass, uvs) + uvs + uks self.O = compute_hardened_hash(opass, ovs, self.U) + ovs + oks ukey = compute_hardened_hash(upass, uks) okey = compute_hardened_hash(opass, oks, self.U) end self.UE = AES.new(ukey, iv, false).encrypt(file_key)[iv.size, 32] self.OE = AES.new(okey, iv, false).encrypt(file_key)[iv.size, 32] perms = [ self.P ].pack("V") + # 0-3 [ -1 ].pack("V") + # 4-7 (self.EncryptMetadata == true ? "T" : "F") + # 8 "adb" + # 9-11 [ 0 ].pack("V") # 12-15 self.Perms = AES.new(file_key, iv, false).encrypt(perms)[iv.size, 16] file_key end end # # Checks user password. # For version 2,3 and 4, _salt_ is the document ID. # For version 5 and 6, _salt_ is the User Key Salt. # def is_user_password?(pass, salt) if self.R == 2 compute_user_password(pass, salt) == self.U elsif self.R == 3 or self.R == 4 compute_user_password(pass, salt)[0, 16] == self.U[0, 16] elsif self.R == 5 uvs = self.U[32, 8] Digest::SHA256.digest(password_to_utf8(pass) + uvs) == self.U[0, 32] elsif self.R == 6 uvs = self.U[32, 8] compute_hardened_hash(password_to_utf8(pass), uvs) == self.U[0, 32] end end # # Checks owner password. # For version 2,3 and 4, _salt_ is the document ID. # For version 5, _salt_ is (Owner Key Salt + U) # def is_owner_password?(pass, salt) if self.R < 5 user_password = retrieve_user_password(pass) is_user_password?(user_password, salt) elsif self.R == 5 ovs = self.O[32, 8] Digest::SHA256.digest(password_to_utf8(pass) + ovs + self.U) == self.O[0, 32] elsif self.R == 6 ovs = self.O[32, 8] compute_hardened_hash(password_to_utf8(pass), ovs, self.U[0,48]) == self.O[0, 32] end end # # Retrieve user password from owner password. # Cannot be used with revision 5. # def retrieve_user_password(ownerpassword) key = compute_owner_key(ownerpassword) if self.R == 2 RC4.decrypt(key, self.O) elsif self.R == 3 or self.R == 4 user_password = RC4.decrypt(xor(key, 19), self.O) 19.times { |i| user_password = RC4.decrypt(xor(key, 18-i), user_password) } user_password end end private # # Used to encrypt/decrypt the O field. # Rev 2,3,4: O = crypt(user_pass, owner_key). # Rev 5: unused. # def compute_owner_key(ownerpassword) #:nodoc: opadded = pad_password(ownerpassword) hash = Digest::MD5.digest(opadded) 50.times { hash = Digest::MD5.digest(hash) } if self.R >= 3 if self.R == 2 hash[0, 5] elsif self.R >= 3 hash[0, self.Length / 8] end end # # Compute the value of the U field. # Cannot be used with revision 5. # def compute_user_password(userpassword, salt) #:nodoc: if self.R == 2 key = compute_user_encryption_key(userpassword, salt) user_key = RC4.encrypt(key, PADDING) elsif self.R == 3 or self.R == 4 key = compute_user_encryption_key(userpassword, salt) upadded = PADDING + salt hash = Digest::MD5.digest(upadded) user_key = RC4.encrypt(key, hash) 19.times { |i| user_key = RC4.encrypt(xor(key,i+1), user_key) } user_key.ljust(32, 0xFF.chr) end end # # Computes hardened hash used in revision 6 (extension level 8). # def compute_hardened_hash(password, salt, vector = '') block_size = 32 input = Digest::SHA256.digest(password + salt + vector) + "\x00" * 32 key = input[0, 16] iv = input[16, 16] digest, aes, h, x = nil, nil, nil, nil i = 0 while i < 64 or i < x[-1].ord + 32 block = input[0, block_size] if Origami::OPTIONS[:use_openssl] aes = OpenSSL::Cipher::Cipher.new("aes-128-cbc").encrypt aes.iv = iv aes.key = key aes.padding = 0 else fail "You need OpenSSL support to encrypt/decrypt documents with this method" end 64.times do |j| x = '' x += aes.update(password) unless password.empty? x += aes.update(block) x += aes.update(vector) unless vector.empty? if j == 0 block_size = 32 + (x.unpack("C16").inject(0) {|a,b| a+b} % 3) * 16 digest = Digest::SHA2.new(block_size << 3) end digest.update(x) end h = digest.digest key = h[0, 16] input[0, block_size] = h[0, block_size] iv = h[16, 16] i = i + 1 end h[0, 32] end def xor(str, byte) #:nodoc: str.split(//).map!{|c| (c[0].ord ^ byte).chr }.join end def pad_password(password) #:nodoc: return PADDING.dup if password.empty? # Fix for Ruby 1.9 bug password[0,32].ljust(32, PADDING) end def password_to_utf8(passwd) #:nodoc: LiteralString.new(passwd).to_utf8[0, 127] end end end end end origami-2.0.0/lib/origami/parser.rb0000644000004100000410000001755412757133666017242 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end require 'colorize' require 'strscan' module Origami class Parser #:nodoc: class ParsingError < Error #:nodoc: end # # Do not output debug information. # VERBOSE_QUIET = 0 # # Output some useful information. # VERBOSE_INFO = 1 # # Output debug information. # VERBOSE_DEBUG = 2 # # Output every objects read # VERBOSE_TRACE = 3 attr_accessor :options def initialize(options = {}) #:nodoc: # Type information for indirect objects. @deferred_casts = {} #Default options values @options = { verbosity: VERBOSE_INFO, # Verbose level. ignore_errors: true, # Try to keep on parsing when errors occur. callback: Proc.new {}, # Callback procedure whenever a structure is read. logger: STDERR, # Where to output parser messages. colorize_log: true # Colorize parser output? } @options.update(options) @logger = @options[:logger] @data = nil ::String.disable_colorization(false) if @options[:colorize_log] end def pos raise RuntimeError, "Cannot get position, parser has no loaded data." if @data.nil? @data.pos end def pos=(offset) raise RuntimeError, "Cannot set position, parser has no loaded data." if @data.nil? @data.pos = offset end def parse(stream) data = if stream.respond_to? :read StringScanner.new(stream.read.force_encoding('binary')) elsif stream.is_a? ::String @filename = stream StringScanner.new(File.binread(@filename)) elsif stream.is_a? StringScanner stream else raise TypeError end @data = data @data.pos = 0 end def parse_object(pos = @data.pos) #:nodoc: @data.pos = pos begin obj = Object.parse(@data, self) return if obj.nil? if Origami::OPTIONS[:enable_type_propagation] and @deferred_casts.key?(obj.reference) types = @deferred_casts[obj.reference] types = [ types ] unless types.is_a?(::Array) # Promote object if a compatible type is found. if cast_type = types.find{|type| type < obj.class} obj = obj.cast_to(cast_type, self) end end trace "Read #{obj.type} object#{ if obj.class != obj.native_type " (" + obj.native_type.to_s.split('::').last + ")" end }, #{obj.reference}" @options[:callback].call(obj) obj rescue UnterminatedObjectError error $!.message obj = $!.obj Object.skip_until_next_obj(@data) @options[:callback].call(obj) obj rescue error "Breaking on: #{(@data.peek(10) + "...").inspect} at offset 0x#{@data.pos.to_s(16)}" error "Last exception: [#{$!.class}] #{$!.message}" if not @options[:ignore_errors] error "Manually fix the file or set :ignore_errors parameter." raise end debug 'Skipping this indirect object.' raise if not Object.skip_until_next_obj(@data) retry end end def parse_xreftable(pos = @data.pos) #:nodoc: @data.pos = pos begin info "...Parsing xref table..." xreftable = XRef::Section.parse(@data) @options[:callback].call(xreftable) xreftable rescue debug "Exception caught while parsing xref table : " + $!.message warn "Unable to parse xref table! Xrefs might be stored into an XRef stream." @data.pos -= 'trailer'.length unless @data.skip_until(/trailer/).nil? nil end end def parse_trailer(pos = @data.pos) #:nodoc: @data.pos = pos begin info "...Parsing trailer..." trailer = Trailer.parse(@data, self) @options[:callback].call(trailer) trailer rescue debug "Exception caught while parsing trailer : " + $!.message warn "Unable to parse trailer!" raise end end def defer_type_cast(reference, type) #:nodoc: @deferred_casts[reference] = type end def target_filename @filename end def target_filesize @data.string.size if @data end def target_data @data.string.dup if @data end private def error(str = "") #:nodoc: if @options[:colorize_log] @logger.puts "[error] #{str}".red else @logger.puts "[error] #{str}" end end def warn(str = "") #:nodoc: return unless @options[:verbosity] >= VERBOSE_INFO if @options[:colorize_log] @logger.puts "[info ] Warning: #{str}".yellow else @logger.puts "[info ] Warning: #{str}" end end def info(str = "") #:nodoc: return unless @options[:verbosity] >= VERBOSE_INFO if @options[:colorize_log] @logger.print "[info ] ".green @logger.puts str else @logger.puts "[info ] #{str}" end end def debug(str = "") #:nodoc: return unless @options[:verbosity] >= VERBOSE_DEBUG if @options[:colorize_log] @logger.print "[debug] ".magenta @logger.puts str else @logger.puts "[debug] #{str}" end end def trace(str = "") #:nodoc: return unless @options[:verbosity] >= VERBOSE_TRACE if @options[:colorize_log] @logger.print "[trace] ".cyan @logger.puts str else @logger.puts "[trace] #{str}" end end def propagate_types(document) info "...Propagating types..." current_state = nil until current_state == @deferred_casts current_state = @deferred_casts.clone current_state.each_pair do |ref, type| type = [ type ] unless type.is_a?(::Array) type.each do |hint| break if document.cast_object(ref, hint, self) end end end end end end origami-2.0.0/lib/origami/metadata.rb0000644000004100000410000001317512757133666017521 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end require 'rexml/document' module Origami class PDF # # Returns true if the document has a document information dictionary. # def document_info? trailer_key? :Info end # # Returns the document information dictionary if present. # def document_info trailer_key :Info end def title; get_document_info_field(:Title) end def author; get_document_info_field(:Author) end def subject; get_document_info_field(:Subject) end def keywords; get_document_info_field(:Keywords) end def creator; get_document_info_field(:Creator) end def producer; get_document_info_field(:Producer) end def creation_date; get_document_info_field(:CreationDate) end def mod_date; get_document_info_field(:ModDate) end # # Returns true if the document has a catalog metadata stream. # def metadata? self.Catalog.Metadata.is_a?(Stream) end # # Returns a Hash of the information found in the metadata stream # def metadata metadata_stm = self.Catalog.Metadata if metadata_stm.is_a?(Stream) doc = REXML::Document.new(metadata_stm.data) info = {} doc.elements.each('*/*/rdf:Description') do |description| description.attributes.each_attribute do |attr| case attr.prefix when 'pdf','xap' info[attr.name] = attr.value end end description.elements.each('*') do |element| value = (element.elements['.//rdf:li'] || element).text info[element.name] = value.to_s end end info end end # # Modifies or creates a metadata stream. # def create_metadata(info = {}) skeleton = <<-XMP XMP xml = if self.Catalog.Metadata.is_a?(Stream) self.Catalog.Metadata.data else skeleton end doc = REXML::Document.new(xml) desc = doc.elements['*/*/rdf:Description'] info.each do |name, value| elt = REXML::Element.new "pdf:#{name}" elt.text = value desc.elements << elt end xml = ""; doc.write(xml, 4) if self.Catalog.Metadata.is_a?(Stream) self.Catalog.Metadata.data = xml else self.Catalog.Metadata = Stream.new(xml) end self.Catalog.Metadata end private def get_document_info_field(field) #:nodoc: if self.document_info? doc_info = self.document_info if doc_info.key?(field) case obj = doc_info[field].solve when String then obj.value when Stream then obj.data end end end end end # # Class representing an information Dictionary, containing title, author, date of creation and the like. # class Metadata < Dictionary include StandardObject field :Title, :Type => String, :Version => "1.1" field :Author, :Type => String field :Subject, :Type => String, :Version => "1.1" field :Keywords, :Type => String, :Version => "1.1" field :Creator, :Type => String field :Producer, :Type => String field :CreationDate, :Type => String field :ModDate, :Type => String, :Version => "1.1" field :Trapped, :Type => Name, :Default => :Unknown, :Version => "1.3" end # # Class representing a metadata Stream. # This stream can contain the same information as the Metadata dictionary, but is storing in XML data. # class MetadataStream < Stream include StandardObject field :Type, :Type => Name, :Default => :Metadata, :Required => true field :Subtype, :Type => Name, :Default =>:XML, :Required => true end end origami-2.0.0/lib/origami/javascript.rb0000644000004100000410000006205312757133666020106 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end module Origami begin require 'v8' class V8::Object #def inspect # case self # when V8::Array,V8::Function then super # else # "{#{self.to_a.map{|k,v| "#{k}:#{v.inspect}"}.join(', ')}}" # end #end end class PDF module JavaScript module Platforms WINDOWS = "WIN" UNIX = "UNIX" MAC = "MAC" end module Viewers ADOBE_READER = "Reader" end class Error < Origami::Error; end class MissingArgError < Error def initialize; super("Missing required argument.") end end class TypeError < Error def initialize; super("Incorrect argument type.") end end class InvalidArgsError < Error def initialize; super("Incorrect arguments.") end end class NotAllowedError < Error def initialize; super("Security settings prevent access to this property or method.") end end class HelpError < Error def initialize; super("Help") end end class GeneralError < Error def initialize; super("Operation failed.") end end class Arg attr_reader :name, :type, :required, :default def initialize(declare = {}) @name = declare[:name] @type = declare[:type] @required = declare[:required] @default = declare[:default] end def self.[](declare = {}) self.new(declare) end def self.inspect(obj) case obj when V8::Function then "function #{obj.name}" when V8::Array then obj.to_a.inspect when V8::Object "{#{obj.to_a.map{|k,v| "#{k}:#{Arg.inspect(v)}"}.join(', ')}}" else obj.inspect end end end class AcrobatObject def initialize(engine) @engine = engine end def self.check_method_args(args, def_args) if args.first.is_a?(V8::Object) args = args.first members = args.entries.map{|k,v| k} argv = [] def_args.each do |def_arg| raise MissingArgError if def_arg.required and not members.include?(def_arg.name) if members.include?(def_arg.name) arg = args[def_arg.name] raise TypeError if def_arg.type and not arg.is_a?(def_arg.type) else arg = def_arg.default end argv.push(arg) end args = argv else i = 0 def_args.each do |def_arg| raise MissingArgError if def_arg.required and i >= args.length raise TypeError if def_arg.type and not args[i].is_a?(def_arg.type) args.push(def_arg.default) if i >= args.length i = i + 1 end end args end def self.acro_method(name, *def_args, &b) define_method(name) do |*args| if @engine.options[:log_method_calls] @engine.options[:console].puts( "LOG: #{self.class}.#{name}(#{args.map{|arg| Arg.inspect(arg)}.join(',')})" ) end args = AcrobatObject.check_method_args(args, def_args) self.instance_exec(*args, &b) if b end end def self.acro_method_protected(name, *def_args, &b) define_method(name) do |*args| if @engine.options[:log_method_calls] @engine.options[:console].puts( "LOG: #{self.class}.#{name}(#{args.map{|arg| arg.inspect}.join(',')})" ) end raise NotAllowedError args = AcrobatObject.check_method_args(args, def_args) self.instance_exec(*args, &b) if b end end def to_s "[object #{self.class.to_s.split('::').last}]" end alias inspect to_s end class AcroTimer < AcrobatObject def initialize(engine, timeout, code, repeat) @thr = Thread.start(engine, timeout, code, repeat) do loop do sleep(timeout / 1000.0) engine.exec(code) break if not repeat end end end end class TimeOut < AcroTimer def initialize(engine, timeout, code) super(engine, timeout, code, false) end end class Interval < AcroTimer def initialize(engine, timeout, code) super(engine, timeout, code, true) end end class ReadStream < AcrobatObject def initialize(engine, data) super(engine) @data = data end acro_method 'read', Arg[name: 'nBytes', type: Numeric, required: true] do |nBytes| @data.slice!(0, nBytes).unpack("H*")[0] end end class Acrohelp < AcrobatObject; end class Global < AcrobatObject def initialize(engine) super(engine) @vars = {} end def []=(name, value) @vars[name] ||= {callbacks: []} @vars[name][:value] = value @vars[name][:callbacks].each do |callback| callback.call(value) end end def [](name) @vars[name][:value] if @vars.include?(name) end acro_method 'setPersistent', Arg[name: 'cVariable', required: true], Arg[name: 'bPersist', required: true] do |cVariable, bPersist| raise GeneralError unless @vars.include?(cVariable) end acro_method 'subscribe', Arg[name: 'cVariable', required: true], Arg[name: 'fCallback', type: V8::Function, require: true] do |cVariable, fCallback| if @vars.include?(cVariable) @vars[cVariable][:callbacks].push(fCallback) fCallback.call(@vars[cVariable][:value]) end end end class Doc < AcrobatObject attr_reader :info attr_accessor :disclosed attr_reader :hidden attr_reader :app, :acrohelp, :global, :console, :util class Info < AcrobatObject def initialize(engine, doc) super(engine) @doc = doc end def title; @doc.title.to_s end def author; @doc.author.to_s end def subject; @doc.subject.to_s end def keywords; @doc.keywords.to_s end def creator; @doc.creator.to_s end def creationDate; @doc.creation_date.to_s end def modDate; @doc.mod_date.to_s end end def initialize(*args) engine, pdf = args # XXX: Bypass therubyracer bug #238. Temporary. super(engine) @pdf = pdf @disclosed = false @hidden = false @info = Info.new(@engine, pdf) @app = JavaScript::App.new(@engine) @acrohelp = JavaScript::Acrohelp.new(@engine) @global = JavaScript::Global.new(@engine) @console = JavaScript::Console.new(@engine) @util = JavaScript::Util.new(@engine) end ### PROPERTIES ### def numFields fields = @pdf.fields fields.size end def numPages; @pdf.pages.size end def title; @info.title end def author; @info.author end def subject; @info.subject end def keywords; @info.keywords end def creator; @info.creator end def creationDate; @info.creationDate end def modDate; @info.modDate end def metadata meta = @pdf.Catalog.Metadata (meta.data if meta.is_a?(Stream)).to_s end def filesize; @pdf.original_filesize end def path; @pdf.original_filename.to_s end def documentFileName; File.basename(self.path) end def URL; "file://#{self.path}" end def baseURL; '' end def dataObjects data_objs = [] @pdf.each_attachment do |name, file_desc| if file_desc and file_desc.EF and (f = file_desc.EF.F) data_objs.push Data.new(@engine, name, f.data.size) if f.is_a?(Stream) end end data_objs end ### METHODS ### acro_method 'closeDoc' acro_method 'getDataObject', Arg[name: 'cName', type: ::String, required: true] do |cName| file_desc = @pdf.resolve_name(Names::EMBEDDED_FILES, cName) if file_desc and file_desc.EF and (f = file_desc.EF.F) Data.new(@engine, cName, f.data.size) if f.is_a?(Stream) else raise TypeError end end acro_method 'getDataObjectContents', Arg[name: 'cName', type: ::String, required: true], Arg[name: 'bAllowAuth', default: false] do |cName, bAllowAuth| file_desc = @pdf.resolve_name(Names::EMBEDDED_FILES, cName) if file_desc and file_desc.EF and (f = file_desc.EF.F) ReadStream.new(@engine, f.data) if f.is_a?(Stream) else raise TypeError end end acro_method 'exportDataObject', Arg[name: 'cName', type: ::String, required: true], Arg[name: 'cDIPath' ], Arg[name: 'bAllowAuth'], Arg[name: 'nLaunch'] do |cName, cDIPath, bAllowAuth, nLaunch| file_desc = @pdf.resolve_name(Names::EMBEDDED_FILES, cName) if file_desc and file_desc.EF and (f = file_desc.EF.F) else raise TypeError end raise TypeError if f.nil? end acro_method 'getField', Arg[name: 'cName', type: ::Object, required: true] do |cName| field = @pdf.get_field(cName) Field.new(@engine, field) if field end acro_method 'getNthFieldName', Arg[name: 'nIndex', type: ::Object, required: true] do |nIndex| nIndex = case nIndex when false then 0 when true then 1 else @engine.parseInt.call(nIndex) end raise TypeError if (nIndex.is_a?(Float) and nIndex.nan?) or nIndex < 0 fields = @pdf.fields if fields and nIndex <= fields.size - 1 Field.new(@engine, fields.take(nIndex + 1).last).name.to_s else "" end end end class App < AcrobatObject def platform; @engine.options[:platform] end def viewerType; @engine.options[:viewerType] end def viewerVariation; @engine.options[:viewerVariation] end def viewerVersion; @engine.options[:viewerVersion] end def activeDocs; [] end ### METHODS ### acro_method 'setInterval', Arg[name: 'cExpr', required: true], Arg[name: 'nMilliseconds', type: Numeric, required: true] do |cExpr, nMilliseconds| cExpr = cExpr.is_a?(::String) ? cExpr : '' Interval.new(@engine, nMilliseconds, cExpr) end acro_method 'setTimeOut', Arg[name: 'cExpr', required: true], Arg[name: 'nMilliseconds', type: Numeric, required: true] do |cExpr, nMilliseconds| cExpr = cExpr.is_a?(::String) ? cExpr : '' TimeOut.new(@engine, nMilliseconds, cExpr) end acro_method 'clearInterval', Arg[name: 'oInterval', type: Interval, required: true] do |oInterval| oInterval.instance_variable_get(:@thr).terminate nil end acro_method 'clearTimeOut', Arg[name: 'oInterval', type: TimeOut, required: true] do |oInterval| oInterval.instance_variable_get(:@thr).terminate nil end acro_method_protected 'addMenuItem' acro_method_protected 'addSubMenu' acro_method 'addToolButton' acro_method_protected 'beginPriv' acro_method 'beep' acro_method_protected 'browseForDoc' acro_method_protected 'endPriv' end class Console < AcrobatObject def println(*args) raise MissingArgError unless args.length > 0 @engine.options[:console].puts(args.first.to_s) end acro_method 'show' acro_method 'clear' acro_method 'hide' end class Util < AcrobatObject acro_method 'streamFromString', Arg[name: 'cString', type: ::Object, required: true], Arg[name: 'cCharset', type: ::Object, default: 'utf-8'] do |cString, cCharset| ReadStream.new(@engine, cString.to_s) end acro_method 'stringFromStream', Arg[name: 'oStream', type: ReadStream, required: true], Arg[name: 'cCharset', type: ::Object, default: 'utf-8'] do |oStream, cCharset| oStream.instance_variable_get(:@data).dup end end class Field < AcrobatObject def initialize(engine, field) super(engine) @field = field end def doc; Doc.new(@field.document) end def name (@field.T.value if @field.has_key?(:T)).to_s end def value @field.V.value if @field.has_key?(:V) end def valueAsString self.value.to_s end def type (if @field.has_key?(:FT) case @field.FT.value when PDF::Field::Type::BUTTON if @fields.has_key?(:Ff) flags = @field.Ff.value if (flags & Origami::Annotation::Widget::Button::Flags::PUSHBUTTON) != 0 'button' elsif (flags & Origami::Annotation::Widget::Button::Flags::RADIO) != 0 'radiobox' else 'checkbox' end end when PDF::Field::Type::TEXT then 'text' when PDF::Field::Type::SIGNATURE then 'signature' when PDF::Field::Type::CHOICE if @field.has_key?(:Ff) if (@field.Ff.value & Origami::Annotation::Widget::Choice::Flags::COMBO).zero? 'listbox' else 'combobox' end end end end).to_s end end class Data < AcrobatObject attr_reader :name, :path, :size attr_reader :creationDate, :modDate attr_reader :description, :MIMEType def initialize(engine, name, size, path: nil, creationDate: nil, modDate: nil, description: nil, mimeType: nil) super(engine) @name, @path, @size = name, path, size @creationDate, @modDate = creationDate, modDate @description, @MIMEType = description, mimeType end end end class JavaScript::EngineError < Origami::Error; end class JavaScript::Engine attr_reader :doc attr_reader :context attr_reader :options attr_reader :parseInt def initialize(pdf) @options = { viewerVersion: 11.008, viewerType: JavaScript::Viewers::ADOBE_READER, viewerVariation: JavaScript::Viewers::ADOBE_READER, platform: JavaScript::Platforms::WINDOWS, console: STDOUT, log_method_calls: false } @doc = JavaScript::Doc.new(self, pdf) @context = V8::Context.new(with: @doc) @parseInt = V8::Context.new['parseInt'] @hooks = {} end # # Evaluates a JavaScript code in the current context. # def exec(script) @context.eval(script) end # # Set a hook on a JavaScript method. # def hook(name, &callback) ns = name.split('.') previous = @context ns.each do |n| raise JavaScript::EngineError, "#{name} does not exist" if previous.nil? previous = previous[n] end case previous when V8::Function, UnboundMethod, nil then @context[name] = lambda do |*args| callback[previous, *args] end @hooks[name] = [previous, callback] else raise JavaScript::EngineError, "#{name} is not a function" end end # # Removes an existing hook on a JavaScript method. # def unhook(name) @context[name] = @hooks[name][0] if @hooks.has_key?(name) end # # Returns an Hash of all defined members in specified object name. # def members(obj) members = {} list = @context.eval <<-JS (function(base) { var members = []; for (var i in base) members.push([i, base[i]]); return members; })(#{obj}) JS list.each do |var| members[var[0]] = var[1] end members end # # Returns all members in the global scope. # def scope members('this') end # # Binds the V8 remote debugging agent on the specified TCP _port_. # def enable_debugger(port = 5858) V8::C::Debug.EnableAgent("Origami", port) end def debugger_break exec 'debugger' end end end module String # # Evaluates the current String as JavaScript. # def eval_js self.document.eval_js(self.value) end end class Stream # # Evaluates the current Stream as JavaScript. # def eval_js self.document.eval_js(self.data) end end class PDF # # Executes a JavaScript script in the current document context. # def eval_js(code) js_engine.exec(code) end # # Returns the JavaScript engine (if JavaScript support is present). # def js_engine @js_engine ||= PDF::JavaScript::Engine.new(self) end end rescue LoadError # # V8 unavailable. # end end origami-2.0.0/lib/origami/catalog.rb0000644000004100000410000004230312757133666017346 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end module Origami class PDF # # Sets PDF extension level and version. Only supported values are "1.7" and 3. # def set_extension_level(version, level) exts = (self.Catalog.Extensions ||= Extensions.new) exts[:ADBE] = DeveloperExtension.new exts[:ADBE].BaseVersion = Name.new(version) exts[:ADBE].ExtensionLevel = level self end # # Returns the current Catalog Dictionary. # def Catalog cat = trailer_key(:Root) case cat when Catalog then cat when Dictionary then cat.cast_to(Catalog) else raise InvalidPDFError, "Broken catalog" end end # # Sets the current Catalog Dictionary. # def Catalog=(cat) cat = cat.cast_to(Catalog) unless cat.is_a? Catalog delete_object(@revisions.last.trailer[:Root]) if @revisions.last.trailer[:Root] @revisions.last.trailer.Root = self << cat end # # Sets an action to run on document opening. # _action_:: An Action Object. # def onDocumentOpen(action) unless action.is_a?(Action) or action.is_a?(Destination) or action.is_a?(Reference) raise TypeError, "An Action object must be passed." end unless self.Catalog raise InvalidPDFError, "A catalog object must exist to add this action." end self.Catalog.OpenAction = action self end # # Sets an action to run on document closing. # _action_:: A JavaScript Action Object. # def onDocumentClose(action) unless action.is_a?(Action::JavaScript) or action.is_a?(Reference) raise TypeError, "An Action::JavaScript object must be passed." end unless self.Catalog raise InvalidPDFError, "A catalog object must exist to add this action." end self.Catalog.AA ||= CatalogAdditionalActions.new self.Catalog.AA.WC = action self end # # Sets an action to run on document printing. # _action_:: A JavaScript Action Object. # def onDocumentPrint(action) unless action.is_a?(Action::JavaScript) or action.is_a?(Reference) raise TypeError, "An Action::JavaScript object must be passed." end unless self.Catalog raise InvalidPDFError, "A catalog object must exist to add this action." end self.Catalog.AA ||= CatalogAdditionalActions.new self.Catalog.AA.WP = action self end # # Registers an object into a specific Names root dictionary. # _root_:: The root dictionary (see Names::Root) # _name_:: The value name. # _value_:: The value to associate with this name. # def register(root, name, value) self.Catalog.Names ||= Names.new value.set_indirect(true) unless value.is_a?(Reference) namesroot = self.Catalog.Names[root] if namesroot.nil? names = NameTreeNode.new(:Names => []).set_indirect(true) self.Catalog.Names[root] = names names.Names << name << value else namesroot.solve[:Names] << name << value end end # # Retrieve the corresponding value associated with _name_ in # the specified _root_ name directory, or nil if the value does # not exist. # def resolve_name(root, name) namesroot = get_names_root(root) return nil if namesroot.nil? resolve_name_from_node(namesroot, name) end # # Returns a Hash of all names under the specified _root_ name directory. # def names(root) self.each_name(root).to_h end # # Returns an Enumerator of all names under the specified _root_ name directory. # def each_name(root, &block) return enum_for(__method__, root) unless block_given? names_root = get_names_root(root) return if names_root.nil? names_from_node(names_root, &block) self end private def names_from_node(node, browsed_nodes: [], &block) #:nodoc: return if browsed_nodes.any?{|browsed| browsed.equal?(node)} raise InvalidNameTreeError, "node is not a dictionary" unless node.is_a?(Dictionary) browsed_nodes.push(node) if node.has_key?(:Names) # leaf node names = node.Names raise InvalidNameTreeError, "Names must be an Array" unless names.is_a?(Array) raise InvalidNameTreeError, "Odd number of elements" if names.length.odd? for i in 0...names.length/2 yield(names[i * 2].solve, names[i * 2 + 1].solve) end elsif node.has_key?(:Kids) # intermediate node node.Kids.each do |kid| names_from_node(kid.solve, browsed_nodes: browsed_nodes, &block) end end end def resolve_name_from_node(node, name, browsed_nodes: []) #:nodoc: return if browsed_nodes.any?{|browsed| browsed.equal?(node)} raise InvalidNameTreeError, "node is not a Dictionary" unless node.is_a?(Dictionary) browsed_nodes.push(node) if node.has_key?(:Names) # leaf node limits = node.Limits names = node.Names raise InvalidNameTreeError, "Names must be an Array" unless names.is_a?(Array) raise InvalidNameTreeError, "Odd number of elements" if names.length.odd? if limits.is_a?(Array) raise InvalidNameTreeError, "Invalid Limits array" unless limits.length == 2 min, max = limits[0].value, limits[1].value if name.to_str >= min and name.to_str <= max names = Hash[*names] target = names[name] return target && target.solve end else names = Hash[*names] target = names[name] return target && target.solve end elsif node.has_key?(:Kids) # intermediate node raise InvalidNameTreeError, "Kids must be an Array" unless node.Kids.is_a?(Array) node.Kids.each do |kid| kid = kid.solve limits = kid.Limits unless limits.is_a?(Array) and limits.length == 2 raise InvalidNameTreeError, "Invalid Limits array" end min, max = limits[0].value, limits[1].value if name.to_str >= min and name.to_str <= max return resolve_name_from_node(kid, name, browsed_nodes: browsed_nodes) end end end end def get_names_root(root) #:nodoc: namedirs = self.Catalog.Names return nil if namedirs.nil? or namedirs[root].nil? namedirs[root].solve end end module PageLayout #:nodoc: SINGLE = :SinglePage ONE_COLUMN = :OneColumn TWO_COLUMN_LEFT = :TwoColumnLeft TWO_COLUMN_RIGHT = :TwoColumnRight TWO_PAGE_LEFT = :TwoPageLeft TWO_PAGE_RIGHT = :TwoPageRight end module PageMode #:nodoc: NONE = :UseNone OUTLINES = :UseOutlines THUMBS = :UseThumbs FULLSCREEN = :FullScreen OPTIONAL_CONTENT = :UseOC ATTACHMENTS = :UseAttachments end # # Class representing additional actions which can be associated with a Catalog. # class CatalogAdditionalActions < Dictionary include StandardObject field :WC, :Type => Action, :Version => "1.4" field :WS, :Type => Action, :Version => "1.4" field :DS, :Type => Action, :Version => "1.4" field :WP, :Type => Action, :Version => "1.4" field :DP, :Type => Action, :Version => "1.4" end # # Class representing the Names Dictionary of a PDF file. # class Names < Dictionary include StandardObject # # Defines constants for Names tree root entries. # DESTINATIONS = :Dests AP = :AP JAVASCRIPT = :JavaScript PAGES = :Pages TEMPLATES = :Templates IDS = :IDS URLS = :URLS EMBEDDED_FILES = :EmbeddedFiles ALTERNATE_PRESENTATIONS = :AlternatePresentations RENDITIONS = :Renditions XFA_RESOURCES = :XFAResources field DESTINATIONS, :Type => NameTreeNode.of([DestinationDictionary, Destination]), :Version => "1.2" field AP, :Type => NameTreeNode.of(Annotation::AppearanceStream), :Version => "1.3" field JAVASCRIPT, :Type => NameTreeNode.of(Action::JavaScript), :Version => "1.3" field PAGES, :Type => NameTreeNode.of(Page), :Version => "1.3" field TEMPLATES, :Type => NameTreeNode.of(Page), :Version => "1.3" field IDS, :Type => NameTreeNode.of(WebCapture::ContentSet), :Version => "1.3" field URLS, :Type => NameTreeNode.of(WebCapture::ContentSet), :Version => "1.3" field EMBEDDED_FILES, :Type => NameTreeNode.of(FileSpec), :Version => "1.4" field ALTERNATE_PRESENTATIONS, :Type => NameTreeNode, :Version => "1.4" field RENDITIONS, :Type => NameTreeNode, :Version => "1.5" field XFA_RESOURCES, :Type => NameTreeNode.of(XFAStream), :Version => "1.7", :ExtensionLevel => 3 end # # Class representing a leaf in a Name tree. # class NameLeaf < Array.of(String, Object) # # Creates a new leaf in a Name tree. # _hash_:: A hash of couples, associating a Name with an Reference. # def initialize(hash = {}) super(hash.flat_map {|name, obj| [name.dup, obj]}) end end # # Class representing the ViewerPreferences Dictionary of a PDF. # This dictionary modifies the way the UI looks when the file is opened in a viewer. # class ViewerPreferences < Dictionary include StandardObject # Valid values for the Enforce field. module Enforce PRINT_SCALING = :PrintScaling end field :HideToolbar, :Type => Boolean, :Default => false field :HideMenubar, :Type => Boolean, :Default => false field :HideWindowUI, :Type => Boolean, :Default => false field :FitWindow, :Type => Boolean, :Default => false field :CenterWindow, :Type => Boolean, :Default => false field :DisplayDocTitle, :Type => Boolean, :Default => false, :Version => "1.4" field :NonFullScreenPageMode, :Type => Name, :Default => :UseNone field :Direction, :Type => Name, :Default => :L2R field :ViewArea, :Type => Name, :Default => :CropBox, :Version => "1.4" field :ViewClip, :Type => Name, :Default => :CropBox, :Version => "1.4" field :PrintArea, :Type => Name, :Default => :CropBox, :Version => "1.4" field :PrintClip, :Type => Name, :Default => :CropBox, :Version => "1.4" field :PrintScaling, :Type => Name, :Default => :AppDefault, :Version => "1.6" field :Duplex, :Type => Name, :Default => :Simplex, :Version => "1.7" field :PickTrayByPDFSize, :Type => Boolean, :Version => "1.7" field :PrintPageRange, :Type => Array.of(Integer), :Version => "1.7" field :NumCopies, :Type => Integer, :Version => "1.7" field :Enforce, :Type => Array.of(Name), :Version => "1.7", :ExtensionLevel => 3 end class Requirement < Dictionary include StandardObject class Handler < Dictionary include StandardObject module Type JS = :JS NOOP = :NoOp end field :Type, :Type => Name, :Default => :ReqHandler field :S, :Type => Name, :Default => Type::NOOP, :Required => true field :Script, :Type => String end field :Type, :Type => Name, :Default => :Requirement field :S, :Type => Name, :Default => :EnableJavaScripts, :Version => "1.7", :Required => true field :RH, :Type => Array.of(Handler) end # # Class representing a developer extension. # class DeveloperExtension < Dictionary include StandardObject field :Type, :Type => Name, :Default => :DeveloperExtensions field :BaseVersion, :Type => Name, :Required => true field :ExtensionLevel, :Type => Integer, :Required => true end # # Class representing an extension Dictionary. # class Extensions < Dictionary include StandardObject field :Type, :Type => Name, :Default => :Extensions field :ADBE, :Type => DeveloperExtension end # # Class representing the Catalog Dictionary of a PDF file. # class Catalog < Dictionary include StandardObject field :Type, :Type => Name, :Default => :Catalog, :Required => true field :Version, :Type => Name, :Version => "1.4" field :Pages, :Type => PageTreeNode, :Required => true field :PageLabels, :Type => NumberTreeNode.of(PageLabel), :Version => "1.3" field :Names, :Type => Names, :Version => "1.2" field :Dests, :Type => Dictionary, :Version => "1.1" field :ViewerPreferences, :Type => ViewerPreferences, :Version => "1.2" field :PageLayout, :Type => Name, :Default => PageLayout::SINGLE field :PageMode, :Type => Name, :Default => PageMode::NONE field :Outlines, :Type => Outline field :Threads, :Type => Array, :Version => "1.1" field :OpenAction, :Type => [ Array, Dictionary ], :Version => "1.1" field :AA, :Type => CatalogAdditionalActions, :Version => "1.4" field :URI, :Type => Dictionary, :Version => "1.1" field :AcroForm, :Type => InteractiveForm, :Version => "1.2" field :Metadata, :Type => MetadataStream, :Version => "1.4" field :StructTreeRoot, :Type => Dictionary, :Version => "1.3" field :MarkInfo, :Type => Dictionary, :Version => "1.4" field :Lang, :Type => String, :Version => "1.4" field :SpiderInfo, :Type => WebCapture::SpiderInfo, :Version => "1.3" field :OutputIntents, :Type => Array.of(OutputIntent), :Version => "1.4" field :PieceInfo, :Type => Dictionary, :Version => "1.4" field :OCProperties, :Type => Dictionary, :Version => "1.5" field :Perms, :Type => Dictionary, :Version => "1.5" field :Legal, :Type => Dictionary, :Version => "1.5" field :Requirements, :Type => Array.of(Requirement), :Version => "1.7" field :Collection, :Type => Collection, :Version => "1.7" field :NeedsRendering, :Type => Boolean, :Version => "1.7", :Default => false field :Extensions, :Type => Extensions, :Version => "1.7", :ExtensionLevel => 3 def initialize(hash = {}, parser = nil) set_indirect(true) super(hash, parser) end end end origami-2.0.0/lib/origami/tree.rb0000644000004100000410000000371212757133666016674 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end module Origami class InvalidNameTreeError < Error #:nodoc: end # # Class representing a node in a Name tree. # class NameTreeNode < Dictionary include StandardObject field :Kids, :Type => Array.of(self) field :Names, :Type => Array.of(String, Object) field :Limits, :Type => Array.of(String, length: 2) def self.of(klass) return Class.new(self) do field :Kids, :Type => Array.of(self) field :Names, :Type => Array.of(String, klass) end end end # # Class representing a node in a Number tree. # class NumberTreeNode < Dictionary include StandardObject field :Kids, :Type => Array.of(self) field :Nums, :Type => Array.of(Number, Object) field :Limits, :Type => Array.of(Number, length: 2) def self.of(klass) return Class.new(self) do field :Kids, :Type => Array.of(self) field :Nums, :Type => Array.of(Number, klass) end end end end origami-2.0.0/lib/origami/obfuscation.rb0000644000004100000410000001477612757133666020265 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end module Origami module Obfuscator using TypeConversion WHITECHARS = [ " ", "\t", "\r", "\n", "\0" ] OBJECTS = [ Array, Boolean, Dictionary, Integer, Name, Null, Stream, String, Real, Reference ] MAX_INT = 0xFFFFFFFF PRINTABLE = ("!".."9").to_a + (':'..'Z').to_a + ('['..'z').to_a + ('{'..'~').to_a FILTERS = [ :FlateDecode, :RunLengthDecode, :LZWDecode, :ASCIIHexDecode, :ASCII85Decode ] def self.junk_spaces(max_size = 3) length = rand(max_size) + 1 ::Array.new(length) { WHITECHARS[rand(WHITECHARS.size)] }.join end def self.junk_comment(max_size = 15) length = rand(max_size) + 1 junk_comment = ::Array.new(length) { byte = rand(256).chr until (not byte.nil? and byte != "\n" and byte != "\r"); byte }.join "%#{junk_comment}#{EOL}" end def self.junk_object(type = nil) if type.nil? type = OBJECTS[rand(OBJECTS.size)] end unless type.include?(Origami::Object) raise TypeError, "Not a valid object type" end Obfuscator.send("junk_#{type.to_s.split('::').last.downcase}") end def self.junk_array(max_size = 5) length = rand(max_size) + 1 ::Array.new(length) { obj = Obfuscator.junk_object until (not obj.nil? and not obj.is_a?(Stream)) ; obj }.to_o end def self.junk_boolean Boolean.new(rand(2).zero?) end def self.junk_dictionary(max_size = 5) length = rand(max_size) + 1 hash = Hash.new length.times do obj = Obfuscator.junk_object hash[Obfuscator.junk_name] = obj unless obj.is_a?(Stream) end hash.to_o end def self.junk_integer(max = MAX_INT) Integer.new(rand(max + 1)) end def self.junk_name(max_size = 8) length = rand(max_size) + 1 Name.new(::Array.new(length) { PRINTABLE[rand(PRINTABLE.size)] }.join) end def self.junk_null Null.new end def self.junk_stream(max_data_size = 200) chainlen = rand(2) + 1 chain = ::Array.new(chainlen) { FILTERS[rand(FILTERS.size)] } length = rand(max_data_size) + 1 junk_data = ::Array.new(length) { rand(256).chr }.join stm = Stream.new stm.dictionary = Obfuscator.junk_dictionary(5) stm.setFilter(chain) stm.data = junk_data stm end def self.junk_string(max_size = 10) length = rand(max_size) + 1 strtype = (rand(2).zero?) ? LiteralString : HexaString strtype.new(::Array.new(length) { PRINTABLE[rand(PRINTABLE.size)] }.join) end def self.junk_real Real.new(rand * rand(MAX_INT + 1)) end def self.junk_reference(max_no = 300, max_gen = 1) no = rand(max_no) + 1 gen = rand(max_gen) Reference.new(no, gen) end end class Dictionary def to_obfuscated_str content = TOKENS.first + Obfuscator.junk_spaces self.each_pair do |key, value| content << Obfuscator.junk_spaces + key.to_obfuscated_str + Obfuscator.junk_spaces + value.to_obfuscated_str + Obfuscator.junk_spaces end content << TOKENS.last super(content) end end module Object alias :to_obfuscated_str :to_s end class Array def to_obfuscated_str content = TOKENS.first + Obfuscator.junk_spaces self.each do |entry| content << entry.to_o.to_obfuscated_str + Obfuscator.junk_spaces end content << TOKENS.last super(content) end end class Null alias :to_obfuscated_str :to_s end class Boolean alias :to_obfuscated_str :to_s end class Integer alias :to_obfuscated_str :to_s end class Real alias :to_obfuscated_str :to_s end class Reference def to_obfuscated_str refstr = refno.to_s + Obfuscator.junk_spaces + refgen.to_s + Obfuscator.junk_spaces + "R" super(refstr) end end class LiteralString alias :to_obfuscated_str :to_s end class HexaString alias :to_obfuscated_str :to_s end class Name def to_obfuscated_str(prop = 2) name = @value.dup forbiddenchars = [ " ","#","\t","\r","\n","\0","[","]","<",">","(",")","%","/","\\" ] name.gsub!(/./) do |c| if rand(prop) == 0 or forbiddenchars.include?(c) hexchar = c.ord.to_s(16) hexchar = "0" + hexchar if hexchar.length < 2 '#' + hexchar else c end end super(TOKENS.first + name) end end class Stream def to_obfuscated_str content = "" content << @dictionary.to_obfuscated_str content << "stream" + EOL content << self.encoded_data content << EOL << TOKENS.last super(content) end end class Trailer def to_obfuscated_str content = "" if self.has_dictionary? content << TOKENS.first << EOL << @dictionary.to_obfuscated_str << EOL end content << XREF_TOKEN << EOL << @startxref.to_s << EOL << TOKENS.last << EOL content end end end origami-2.0.0/lib/origami/template/0000755000004100000410000000000012757133666017220 5ustar www-datawww-dataorigami-2.0.0/lib/origami/template/patterns.rb0000644000004100000410000000340212757133666021404 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end module Origami module Template class AxialGradient < Graphics::Pattern::Shading::Axial def initialize(from, to, color0, color1, coeff = 1) super() set_indirect(true) x, y = from tx, ty = to c0 = Graphics::Color.to_a(color0) c1 = Graphics::Color.to_a(color1) space = case c0.size when 1 then Graphics::Color::Space::DEVICE_GRAY when 3 then Graphics::Color::Space::DEVICE_RGB when 4 then Graphics::Color::Space::DEVICE_CMYK end f = Function::Exponential.new f.Domain = [ 0.0, 1.0 ] f.N = coeff f.C0, f.C1 = c0, c1 self.ColorSpace = space self.Coords = [ x, y, tx, ty ] self.Function = f self.Extend = [ true, true ] end end end end origami-2.0.0/lib/origami/template/widgets.rb0000644000004100000410000001371612757133666021223 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end module Origami module Template class Button < Annotation::Widget::PushButton def initialize(caption, id: nil, x:, y:, width:, height:) super() set_indirect(true) self.H = Annotation::Widget::Highlight::INVERT self.Rect = [ x, y, x + width, y + height ] self.F = Annotation::Flags::PRINT self.T = id appstm = Annotation::AppearanceStream.new.setFilter(:FlateDecode) appstm.BBox = [ 0, 0, width, height ] appstm.Matrix = [ 1, 0, 0, 1, 0, 0 ] appstm.draw_rectangle(0, 0, width, height, fill: true, stroke: false, fill_color: Graphics::Color::RGB.new(0xE6, 0xE6, 0xFA)) appstm.draw_polygon([[1,1],[1,height-1],[width-1,height-1],[width-2,height-2],[2,height-2],[2,2]], fill: true, stroke: false, fill_color: Graphics::Color::GrayScale.new(1.0)) appstm.draw_polygon([[width-1,height-1],[width-1,1],[1,1],[2,2],[width-2,2],[width-2,height-2]], fill: true, stroke: false, fill_color: Graphics::Color::RGB.new(130, 130, 130)) appstm.draw_rectangle(0.5, 0.5, width-1, height-1, fill: false, stroke: true, stroke_color: Graphics::Color::GrayScale.new(0.0)) text_width = 4.75 * caption.length appstm.write(caption, x: (width - text_width)/2, y: height/2-5, size: 10) appstm.Resources = Resources.new set_normal_appearance(appstm) end end class Edit < Annotation::Widget::Text def initialize(id, x:, y:, width:, height:) super() set_indirect(true) self.Rect = [ x, y, x+width, y+height ] self.F = Annotation::Flags::PRINT self.T = id self.DA = '/F1 12 Tf 0 g' appstm = Annotation::AppearanceStream.new.setFilter(:FlateDecode) appstm.BBox = [ 0, 0, width, height ] appstm.Matrix = [ 1, 0, 0, 1, 0, 0 ] appstm.draw_rectangle(0, 0, width, height, fill: false, stroke: true, stroke_color: Graphics::Color::GrayScale.new(0.0)) appstm.draw_polygon([[1,1],[1,height-1],[width-1,height-1],[width-2,height-2],[2,height-2],[2,2]], fill: true, stroke: false, fill_color: Graphics::Color::RGB.new(130, 130, 130)) appstm.draw_polygon([[width-1,height-1],[width-1,1],[1,1],[2,2],[width-2,2],[width-2,height-2]], fill: true, stroke: false, fill_color: Graphics::Color::GrayScale.new(1.0)) appstm.draw_rectangle(0.5, 0.5, width-1, height-1, fill: false, stroke: true, stroke_color: Graphics::Color::GrayScale.new(0.0)) set_normal_appearance(appstm) end end class MultiLineEdit < Edit def initialize(id, x:, y:, width:, height:) super(id, x: x, y: y, width: width, height: height) self.Ff ||= 0 self.Ff |= Annotation::Widget::Text::Flags::MULTILINE end end class RichTextEdit < MultiLineEdit def initialize(id, x: , y:, width:, height:) super(id, x: x, y: y, width: width, height: height) self.F |= Annotation::Flags::READONLY self.Ff |= (Annotation::Widget::Text::Flags::RICHTEXT | Field::Flags::READONLY) end end class PasswordEdit < Edit def initialize(id, x:, y:, width:, height:) super(id, x: x, y: y, width: width, height: height) self.Ff ||= 0 self.Ff |= Annotation::Widget::Text::Flags::PASSWORD end end class TextPanel < Annotation::FreeText def initialize(id, x:, y:, width:, height:) super() set_indirect(true) self.Rect = [ x, y, x + width, y + height ] self.F = Annotation::Flags::PRINT self.NM = id self.DA = '/F1 12 Tf 0 g' appstm = Annotation::AppearanceStream.new.setFilter(:FlateDecode) appstm.BBox = [ 0, 0, width, height ] appstm.Matrix = [ 1, 0, 0, 1, 0, 0 ] appstm.draw_rectangle(0, 0, width, height, fill: false, stroke: true, stroke_color: Graphics::Color::GrayScale.new(0.0)) appstm.draw_polygon([[1,1],[1,height-1],[width-1,height-1],[width-2,height-2],[2,height-2],[2,2]], fill: true, stroke: false, fill_color: Graphics::Color::RGB.new(130, 130, 130)) appstm.draw_polygon([[width-1,height-1],[width-1,1],[1,1],[2,2],[width-2,2],[width-2,height-2]], fill: true, stroke: false, fill_color: Graphics::Color::GrayScale.new(1.0)) appstm.draw_rectangle(0.5, 0.5, width-1, height-1, fill: false, stroke: true, stroke_color: Graphics::Color::GrayScale.new(0.0)) set_normal_appearance(appstm) end end end end origami-2.0.0/lib/origami/object.rb0000644000004100000410000004642212757133666017210 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end # # Module for parsing/generating PDF files. # module Origami module TypeConversion refine ::Bignum do def to_o Origami::Integer.new(self) end end refine ::Fixnum do def to_o Origami::Integer.new(self) end end refine ::Array do def to_o Origami::Array.new(self) end end refine ::Float do def to_o Origami::Real.new(self) end end refine ::Hash do def to_o Origami::Dictionary.new(self) end end refine ::TrueClass do def to_o Origami::Boolean.new(true) end end refine ::FalseClass do def to_o Origami::Boolean.new(false) end end refine ::NilClass do def to_o Origami::Null.new end end refine ::Symbol do def to_o Origami::Name.new(self) end end refine ::String do def to_o Origami::LiteralString.new(self) end end end # # Common Exception class for Origami errors. # class Error < StandardError end # # Mixin' module for objects which can store their options into an inner Dictionary. # module StandardObject #:nodoc: DEFAULT_ATTRIBUTES = { :Type => Object, :Version => "1.2" } #:nodoc: def self.included(receiver) #:nodoc: receiver.instance_variable_set(:@fields, Hash.new(DEFAULT_ATTRIBUTES)) receiver.extend(ClassMethods) end module ClassMethods #:nodoc:all def inherited(subclass) subclass.instance_variable_set(:@fields, Hash[@fields.map{|name, attributes| [name, attributes.clone]}]) end def fields @fields end def field(name, attributes) if attributes[:Required] == true and attributes.has_key?(:Default) and attributes[:Type] == Name self.add_type_info(self, name, attributes[:Default]) end if @fields.has_key?(name) @fields[name].merge! attributes else @fields[name] = attributes end define_field_methods(name) end def define_field_methods(field) # # Getter method. # getter = field.to_s remove_method(getter) rescue NameError define_method(getter) do obj = self[field] obj.is_a?(Reference) ? obj.solve : obj end # # Setter method. # setter = field.to_s + "=" remove_method(setter) rescue NameError define_method(setter) do |value| self[field] = value end # Setter method returning self. setter_self = "set" + field.to_s remove_method(setter_self) rescue NameError define_method(setter_self) do |value| self[field] = value self end end # # Returns an array of required fields for the current Object. # def required_fields fields = [] @fields.each_pair do |name, attributes| fields << name if attributes[:Required] == true end fields end def hint_type(name) if @fields.has_key?(name) @fields[name][:Type] end end end def pre_build #:nodoc: set_default_values do_type_check if Origami::OPTIONS[:enable_type_checking] == true super end # # Check if an attribute is set in the current Object. # _attr_:: The attribute name. # def has_field? (field) not self[field].nil? end # # Returns the version and level required by the current Object. # def version_required #:nodoc: max = [ 1.0, 0 ] self.each_key do |field| attributes = self.class.fields[field.value] if attributes.nil? STDERR.puts "Warning: object #{self.class} has undocumented field #{field.value}" next end current_version = attributes.has_key?(:Version) ? attributes[:Version].to_f : 0 current_level = attributes[:ExtensionLevel] || 0 current = [ current_version, current_level ] max = current if (current <=> max) > 0 sub = self[field.value].version_required max = sub if (sub <=> max) > 0 end max end def set_default_value(field) #:nodoc: if self.class.fields[field][:Default] self[field] = self.class.fields[field][:Default] self[field].pre_build end end def set_default_values #:nodoc: self.class.required_fields.each do |field| set_default_value(field) unless has_field?(field) end end def do_type_check #:nodoc: self.class.fields.each_pair do |field, attributes| next if self[field].nil? or attributes[:Type].nil? begin field_value = self[field].solve rescue InvalidReferenceError STDERR.puts "Warning: in object #{self.class}, field `#{field.to_s}' is an invalid reference (#{self[field].to_s})" next end types = attributes[:Type].is_a?(::Array) ? attributes[:Type] : [ attributes[:Type] ] unless types.any? {|type| not type.is_a?(Class) or field_value.is_a?(type.native_type)} STDERR.puts "Warning: in object #{self.class}, field `#{field.to_s}' has unexpected type #{field_value.class}" end if attributes.key?(:Assert) and not (attributes[:Assert] === field_value) STDERR.puts "Warning: assertion failed for field `#{field.to_s}' in object #{self.class}" end end end end class InvalidObjectError < Error #:nodoc: end class UnterminatedObjectError < Error #:nodoc: attr_reader :obj def initialize(msg,obj) super(msg) @obj = obj end end WHITESPACES = "([ \\f\\t\\r\\n\\0]|%[^\\n]*\\n)*" #:nodoc: WHITECHARS_NORET = "[ \\f\\t\\0]*" #:nodoc: EOL = "\r\n" #:nodoc: WHITECHARS = "[ \\f\\t\\r\\n\\0]*" #:nodoc: REGEXP_WHITESPACES = Regexp.new(WHITESPACES) #:nodoc: # # Parent module representing a PDF Object. # PDF specification declares a set of primitive object types : # * Null # * Boolean # * Integer # * Real # * Name # * String # * Array # * Dictionary # * Stream # module Object TOKENS = %w{ obj endobj } #:nodoc: @@regexp_obj = Regexp.new(WHITESPACES + "(?\\d+)" + WHITESPACES + "(?\\d+)" + WHITESPACES + TOKENS.first + WHITESPACES) @@regexp_endobj = Regexp.new(WHITESPACES + TOKENS.last + WHITESPACES) attr_accessor :no, :generation, :file_offset, :objstm_offset attr_accessor :parent # # Creates a new PDF Object. # def initialize(*cons) @indirect = false @no, @generation = 0, 0 @document = nil @parent = nil super(*cons) unless cons.empty? end # # Sets whether the object is indirect or not. # Indirect objects are allocated numbers at build time. # def set_indirect(bool) unless bool == true or bool == false raise TypeError, "The argument must be boolean" end if bool == false @no = @generation = 0 @document = nil end @indirect = bool self end # # Generic method called just before the object is finalized. # At this time, no number nor generation allocation has yet been done. # def pre_build self end # # Generic method called just after the object is finalized. # At this time, any indirect object has its own number and generation identifier. # def post_build self end # # Compare two objects from their respective numbers. # def <=>(obj) [@no, @generation] <=> [obj.no, obj.generation] end # # Returns whether the objects is indirect, which means that it is not embedded into another object. # def indirect? @indirect end # # Deep copy of an object. # def copy saved_doc = @document saved_parent = @parent @document = @parent = nil # do not process parent object and document in the copy # Perform the recursive copy (quite dirty). copyobj = Marshal.load(Marshal.dump(self)) # restore saved values @document = saved_doc @parent = saved_parent copyobj.set_document(saved_doc) if copyobj.indirect? copyobj.parent = parent copyobj end # # Returns an indirect reference to this object, or a Null object is this object is not indirect. # def reference raise InvalidObjectError, "Cannot reference a direct object" unless self.indirect? ref = Reference.new(@no, @generation) ref.parent = self ref end # # Returns an array of references pointing to the current object. # def xrefs raise InvalidObjectError, "Cannot find xrefs to a direct object" unless self.indirect? if self.document.nil? raise InvalidObjectError, "Not attached to any document" end refs = [] @document.root_objects.each do |obj| if obj.is_a?(ObjectStream) obj.each do |child| case child when Dictionary, Array refs.concat child.xref_cache[self.reference] if child.xref_cache.key?(self.reference) end end end obj = obj.dictionary if obj.is_a?(Stream) case obj when Dictionary, Array refs.concat obj.xref_cache[self.reference] if obj.xref_cache.key?(self.reference) end end refs end # # Creates an exportable version of current object. # The exportable version is a copy of _self_ with solved references, no owning PDF and no parent. # References to Catalog or PageTreeNode objects have been destroyed. # # When exported, an object can be moved into another document without hassle. # def export exported_obj = self.logicalize exported_obj.no = exported_obj.generation = 0 exported_obj.set_document(nil) if exported_obj.indirect? exported_obj.parent = nil exported_obj.xref_cache.clear exported_obj end # # Returns a logicalized copy of _self_. # See logicalize! # def logicalize #:nodoc: self.copy.logicalize! end # # Transforms recursively every references to the copy of their respective object. # Catalog and PageTreeNode objects are excluded to limit the recursion. # def logicalize! #:nodoc: resolve_all_references = -> (obj, browsed = [], ref_cache = {}) do return if browsed.include?(obj) browsed.push(obj) if obj.is_a?(ObjectStream) obj.each do |subobj| resolve_all_references[obj, browsed, ref_cache] end end if obj.is_a?(Dictionary) or obj.is_a?(Array) obj.map! do |subobj| if subobj.is_a?(Reference) new_obj = if ref_cache.has_key?(subobj) ref_cache[subobj] else ref_cache[subobj] = subobj.solve.copy end new_obj.no = new_obj.generation = 0 new_obj.parent = obj new_obj unless new_obj.is_a?(Catalog) or new_obj.is_a?(PageTreeNode) else subobj end end obj.each do |subobj| resolve_all_references[subobj, browsed, ref_cache] end elsif obj.is_a?(Stream) resolve_all_references[obj.dictionary, browsed, ref_cache] end end resolve_all_references[self] end # # Returns the indirect object which contains this object. # If the current object is already indirect, returns self. # def indirect_parent obj = self obj = obj.parent until obj.indirect? obj end # # Returns self. # def to_o self end # # Returns self. # def solve self end # # Returns the PDF which the object belongs to. # def document if self.indirect? then @document else @parent.document unless @parent.nil? end end def set_document(doc) raise InvalidObjectError, "You cannot set the document of a direct object" unless self.indirect? @document = doc end class << self def typeof(stream, noref = false) #:nodoc: stream.skip(REGEXP_WHITESPACES) case stream.peek(1) when '/' then return Name when '<' return (stream.peek(2) == '<<') ? Stream : HexaString when '(' then return LiteralString when '[' then return Origami::Array when 'n' then return Null if stream.peek(4) == 'null' when 't' then return Boolean if stream.peek(4) == 'true' when 'f' then return Boolean if stream.peek(5) == 'false' else if not noref and stream.check(Reference::REGEXP_TOKEN) then return Reference elsif stream.check(Real::REGEXP_TOKEN) then return Real elsif stream.check(Integer::REGEXP_TOKEN) then return Integer else nil end end nil end def parse(stream, parser = nil) #:nodoc: offset = stream.pos # # End of body ? # return nil if stream.match?(/xref/) or stream.match?(/trailer/) or stream.match?(/startxref/) if stream.scan(@@regexp_obj).nil? raise InvalidObjectError, "Object shall begin with '%d %d obj' statement" end no = stream['no'].to_i gen = stream['gen'].to_i type = typeof(stream) if type.nil? raise InvalidObjectError, "Cannot determine object (no:#{no},gen:#{gen}) type" end begin new_obj = type.parse(stream, parser) rescue raise InvalidObjectError, "Failed to parse object (no:#{no},gen:#{gen})\n\t -> [#{$!.class}] #{$!.message}" end new_obj.set_indirect(true) new_obj.no = no new_obj.generation = gen new_obj.file_offset = offset if stream.skip(@@regexp_endobj).nil? raise UnterminatedObjectError.new("Object shall end with 'endobj' statement", new_obj) end new_obj end def skip_until_next_obj(stream) #:nodoc: [ @@regexp_obj, /xref/, /trailer/, /startxref/ ].each do |re| if stream.scan_until(re) stream.pos -= stream.matched_size return true end end false end end def version_required #:nodoc: [ 1.0, 0 ] end # # Returns the symbol type of this Object. # def type name = (self.class.name or self.class.superclass.name or self.native_type.name) name.split("::").last.to_sym end def self.native_type; Origami::Object end #:nodoc: # # Returns the native PDF type of this Object. # def native_type self.class.native_type end def cast_to(type, _parser = nil) #:nodoc: if type.native_type != self.native_type raise TypeError, "Incompatible cast from #{self.class} to #{type}" end self end # # Outputs this object into PDF code. # _data_:: The object data. # def to_s(data) content = "" content << "#{no} #{generation} #{TOKENS.first}" << EOL if self.indirect? content << data content << EOL << TOKENS.last << EOL if self.indirect? content.force_encoding('binary') end alias output to_s end end origami-2.0.0/lib/origami/dictionary.rb0000644000004100000410000002134712757133666020106 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end module Origami class InvalidDictionaryObjectError < InvalidObjectError #:nodoc: end # # Class representing a Dictionary Object. # Dictionaries are containers associating a Name to an embedded Object. # class Dictionary < Hash include Origami::Object using TypeConversion TOKENS = %w{ << >> } #:nodoc: @@regexp_open = Regexp.new(WHITESPACES + TOKENS.first + WHITESPACES) @@regexp_close = Regexp.new(WHITESPACES + TOKENS.last + WHITESPACES) @@cast_fingerprints = {} @@cast_keys = [] attr_reader :strings_cache, :names_cache, :xref_cache # # Creates a new Dictionary. # _hash_:: The hash representing the new Dictionary. # def initialize(hash = {}, parser = nil) raise TypeError, "Expected type Hash, received #{hash.class}." unless hash.is_a?(Hash) super() @strings_cache = [] @names_cache = [] @xref_cache = {} hash.each_pair do |k,v| next if k.nil? # Turns the values into Objects. key, value = k.to_o, v.to_o if Origami::OPTIONS[:enable_type_guessing] hint_type = guess_value_type(key, value) if hint_type.is_a?(Class) and hint_type < value.class value = value.cast_to(hint_type, parser) end if hint_type and parser and Origami::OPTIONS[:enable_type_propagation] if value.is_a?(Reference) parser.defer_type_cast(value, hint_type) end end end # Cache keys and values for fast search. cache_key(key) cache_value(value) self[key] = value end end def self.parse(stream, parser = nil) #:nodoc: offset = stream.pos if stream.skip(@@regexp_open).nil? raise InvalidDictionaryObjectError, "No token '#{TOKENS.first}' found" end hash = {} while stream.skip(@@regexp_close).nil? do key = Name.parse(stream, parser) type = Object.typeof(stream) raise InvalidDictionaryObjectError, "Invalid object for field #{key.to_s}" if type.nil? value = type.parse(stream, parser) hash[key] = value end if Origami::OPTIONS[:enable_type_guessing] and not (@@cast_keys & hash.keys).empty? dict_type = self.guess_type(hash) else dict_type = self end # Creates the Dictionary. dict = dict_type.new(hash, parser) dict.file_offset = offset dict end def to_s(indent: 1, tab: "\t") #:nodoc: if indent > 0 content = TOKENS.first + EOL self.each_pair do |key,value| content << tab * indent << key.to_s << ' ' content << (value.is_a?(Dictionary) ? value.to_s(indent: indent+1) : value.to_s) content << EOL end content << tab * (indent - 1) << TOKENS.last else content = TOKENS.first.dup self.each_pair do |key,value| content << "#{key.to_s} #{value.is_a?(Dictionary) ? value.to_s(indent: 0) : value.to_s}" end content << TOKENS.last end super(content) end def map!(&b) self.each_pair do |k,v| self[k] = b.call(v) end end def merge(dict) Dictionary.new(super(dict)) end def []=(key,val) unless key.is_a?(Symbol) or key.is_a?(Name) fail "Expecting a Name for a Dictionary entry, found #{key.class} instead." end key = key.to_o if val.nil? delete(key) return end val = val.to_o super(key,val) key.parent = self val.parent = self unless val.indirect? or val.parent.equal?(self) val end def [](key) super(key.to_o) end def key?(key) super(key.to_o) end alias include? key? alias has_key? key? def delete(key) super(key.to_o) end def cast_to(type, parser = nil) super(type) cast = type.new(self, parser) cast.parent = self.parent cast.no, cast.generation = self.no, self.generation if self.indirect? cast.set_indirect(true) cast.set_document(self.document) cast.file_offset = self.file_offset # cast can replace self end cast end alias each each_value def to_h Hash[self.to_a.map!{|k, v| [ k.value, v.value ]}] end alias value to_h def method_missing(field, *args) #:nodoc: raise NoMethodError, "No method `#{field}' for #{self.class}" unless field.to_s[0,1] =~ /[A-Z]/ if field.to_s[-1,1] == '=' self[field.to_s[0..-2].to_sym] = args.first else obj = self[field]; obj.is_a?(Reference) ? obj.solve : obj end end def copy copy = self.class.new self.each_pair do |k,v| copy[k] = v.copy end copy.parent = @parent copy.no, copy.generation = @no, @generation copy.set_indirect(true) if self.indirect? copy.set_document(@document) if self.indirect? copy end def self.native_type; Dictionary end def self.add_type_info(klass, key, value) #:nodoc: raise TypeError, "Invalid class #{klass}" unless klass.is_a?(Class) and klass < Dictionary key, value = key.to_o, value.to_o # Inherit the superclass type information. if not @@cast_fingerprints.key?(klass) and @@cast_fingerprints.key?(klass.superclass) @@cast_fingerprints[klass] = @@cast_fingerprints[klass.superclass].dup end @@cast_fingerprints[klass] ||= {} @@cast_fingerprints[klass][key] = value @@cast_keys.push(key) unless @@cast_keys.include?(key) end def self.guess_type(hash) #:nodoc: best_type = self @@cast_fingerprints.each_pair do |klass, keys| next unless klass < best_type best_type = klass if keys.all? { |k,v| hash[k] == v } end best_type end def self.hint_type(name); nil end #:nodoc: private def cache_key(key) @names_cache.push(key) end def cache_value(value) case value when String then @strings_cache.push(value) when Name then @names_cache.push(value) when Reference then (@xref_cache[value] ||= []).push(self) when Dictionary, Array @strings_cache.concat(value.strings_cache) @names_cache.concat(value.names_cache) @xref_cache.update(value.xref_cache) do |_ref, cache1, cache2| cache1.concat(cache2) end value.strings_cache.clear value.names_cache.clear value.xref_cache.clear end end def guess_value_type(key, value) hint_type = self.class.hint_type(key.value) if hint_type.is_a?(::Array) and not value.is_a?(Reference) # Choose best match hint_type = hint_type.find {|type| type < value.class } end hint_type end end end origami-2.0.0/lib/origami/parsers/0000755000004100000410000000000012757133666017064 5ustar www-datawww-dataorigami-2.0.0/lib/origami/parsers/pdf.rb0000644000004100000410000000611012757133666020160 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end require 'origami/parser' module Origami class PDF class Parser < Origami::Parser def initialize(params = {}) options = { password: '', # Default password being tried when opening a protected document. prompt_password: lambda do # Callback procedure to prompt password when document is encrypted. require 'io/console' STDERR.print "Password: " STDIN.noecho(&:gets).chomp end, force: false # Force PDF header detection }.update(params) super(options) end private def parse_initialize #:nodoc: if @options[:force] == true @data.skip_until(/%PDF-/).nil? @data.pos = @data.pos - 5 end pdf = PDF.new(self) info "...Reading header..." begin pdf.header = PDF::Header.parse(@data) @options[:callback].call(pdf.header) rescue InvalidHeaderError raise unless @options[:ignore_errors] warn "PDF header is invalid, ignoring..." end pdf end def parse_finalize(pdf) #:nodoc: warn "This file has been linearized." if pdf.linearized? propagate_types(pdf) if Origami::OPTIONS[:enable_type_propagation] # # Decrypt encrypted file contents # if pdf.encrypted? warn "This document contains encrypted data!" passwd = @options[:password] begin pdf.decrypt(passwd) rescue EncryptionInvalidPasswordError if passwd.empty? passwd = @options[:prompt_password].call retry unless passwd.empty? end raise end end warn "This document has been signed!" if pdf.signed? pdf end end end end origami-2.0.0/lib/origami/parsers/pdf/0000755000004100000410000000000012757133666017635 5ustar www-datawww-dataorigami-2.0.0/lib/origami/parsers/pdf/linear.rb0000644000004100000410000000516312757133666021441 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end require 'origami/parsers/pdf' module Origami class PDF # # Create a new PDF linear Parser. # class LinearParser < Parser def parse(stream) super pdf = parse_initialize # # Parse each revision # revision = 0 until @data.eos? do begin pdf.add_new_revision unless revision.zero? revision = revision + 1 info "...Parsing revision #{pdf.revisions.size}..." loop do break if (object = parse_object).nil? pdf.insert(object) end pdf.revisions.last.xreftable = parse_xreftable trailer = parse_trailer pdf.revisions.last.trailer = trailer if trailer.startxref != 0 xrefstm = pdf.get_object_by_offset(trailer.startxref) elsif trailer[:XRefStm].is_a?(Integer) xrefstm = pdf.get_object_by_offset(trailer[:XRefStm]) end if xrefstm.is_a?(XRefStream) warn "Found a XRefStream for this revision at #{xrefstm.reference}" pdf.revisions.last.xrefstm = xrefstm end rescue error "Cannot read : " + (@data.peek(10) + "...").inspect error "Stopped on exception : " + $!.message break end end pdf.loaded! parse_finalize(pdf) end end end end origami-2.0.0/lib/origami/parsers/pdf/lazy.rb0000644000004100000410000001101212757133666021134 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end require 'origami/parsers/pdf' module Origami class PDF # # Create a new PDF lazy Parser. # class LazyParser < Parser def parse(stream) super pdf = parse_initialize revisions = [] # Set the scanner position at the end. @data.terminate # Locate the startxref token. until @data.match?(/#{Trailer::XREF_TOKEN}/) raise ParsingError, "No xref token found" if @data.pos == 0 @data.pos -= 1 end # Extract the offset of the last xref section. trailer = Trailer.parse(@data, self) raise ParsingError, "Cannot locate xref section" if trailer.startxref.zero? xref_offset = trailer.startxref while xref_offset and xref_offset != 0 # Create a new revision based on the xref section offset. revision = parse_revision(pdf, xref_offset) # Locate the previous xref section. if revision.xrefstm xref_offset = revision.xrefstm[:Prev].to_i else xref_offset = revision.trailer[:Prev].to_i end # Prepend the revision. revisions.unshift(revision) end pdf.revisions.clear revisions.each do |rev| pdf.revisions.push(rev) pdf.insert(rev.xrefstm) if rev.has_xrefstm? end parse_finalize(pdf) pdf end private def parse_revision(pdf, offset) raise ParsingError, "Invalid xref offset" if offset < 0 or offset >= @data.string.size @data.pos = offset # Create a new revision. revision = PDF::Revision.new(pdf) # Regular xref section. if @data.match?(/#{XRef::Section::TOKEN}/) xreftable = parse_xreftable raise ParsingError, "Cannot parse xref section" if xreftable.nil? revision.xreftable = xreftable revision.trailer = parse_trailer # Handle hybrid cross-references. if revision.trailer[:XRefStm].is_a?(Integer) begin offset = revision.trailer[:XRefStm].to_i xrefstm = parse_object(offset) if xrefstm.is_a?(XRefStream) revision.xrefstm = xrefstm else warn "Invalid xref stream at offset #{offset}" end rescue warn "Cannot parse xref stream at offset #{offset}" end end # The xrefs are stored in a stream. else xrefstm = parse_object raise ParsingError, "Invalid xref stream" unless xrefstm.is_a?(XRefStream) revision.xrefstm = xrefstm # Search for the trailer. if @data.skip_until Regexp.union(Trailer::XREF_TOKEN, *Trailer::TOKENS) @data.pos -= @data.matched_size revision.trailer = parse_trailer else warn "No trailer found." revision.trailer = Trailer.new end end revision end end end end origami-2.0.0/lib/origami/parsers/ppklite.rb0000644000004100000410000000350512757133666021064 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end require 'origami/parser' module Origami class PPKLite class Parser < Origami::Parser def parse(stream) #:nodoc: super address_book = PPKLite.new(self) address_book.header = PPKLite::Header.parse(@data) @options[:callback].call(address_book.header) loop do break if (object = parse_object).nil? address_book.insert(object) end address_book.revisions.first.xreftable = parse_xreftable address_book.revisions.first.trailer = parse_trailer if Origami::OPTIONS[:enable_type_propagation] trailer = address_book.revisions.first.trailer if trailer[:Root].is_a?(Reference) address_book.cast_object(trailer[:Root], PPKLite::Catalog, self) end propagate_types(address_book) end address_book end end end end origami-2.0.0/lib/origami/parsers/fdf.rb0000644000004100000410000000333412757133666020153 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end require 'origami/parser' module Origami class FDF class Parser < Origami::Parser def parse(stream) #:nodoc: super(stream) fdf = FDF.new fdf.header = FDF::Header.parse(@data) @options[:callback].call(fdf.header) loop do break if (object = parse_object).nil? fdf.insert(object) end fdf.revisions.first.xreftable = parse_xreftable fdf.revisions.first.trailer = parse_trailer if Origami::OPTIONS[:enable_type_propagation] trailer = fdf.revisions.first.trailer if trailer[:Root].is_a?(Reference) fdf.cast_object(trailer[:Root], FDF::Catalog, self) end propagate_types(fdf) end fdf end end end end origami-2.0.0/lib/origami/webcapture.rb0000644000004100000410000000705112757133666020076 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end module Origami module WebCapture class CommandSettings < Dictionary include StandardObject field :G, :Type => Dictionary field :C, :Type => Dictionary end class Command < Dictionary include StandardObject module Flags SAMESITE = 1 << 1 SAMEPATH = 1 << 2 SUBMIT = 1 << 3 end field :URL, :Type => String, :Required => true field :L, :Type => Integer, :Default => 1 field :F, :Type => Integer, :Default => 0 field :P, :Type => [ String, Stream ] field :CT, :Type => String, :Default => "application/x-www-form-urlencoded" field :H, :Type => String field :S, :Type => CommandSettings end class SourceInformation < Dictionary include StandardObject module SubmissionType NOFORM = 0 GETFORM = 1 POSTFORM = 2 end field :AU, :Type => [ String, Dictionary ], :Required => true field :TS, :Type => String field :E, :Type => String field :S, :Type => Integer, :Default => 0 field :C, :Type => Command end class SpiderInfo < Dictionary include StandardObject field :V, :Type => Real, :Default => 1.0, :Version => "1.3", :Required => true field :C, :Type => Array.of(Command) end class ContentSet < Dictionary include StandardObject PAGE_SET = :SPS IMAGE_SET = :SIS field :Type, :Type => Name, :Default => :SpiderContentSet field :S, :Type => Name, :Required => true field :ID, :Type => String, :Required => true field :O, :Type => Array, :Required => true field :SI, :Type => [ SourceInformation, Array.of(SourceInformation) ], :Required => true field :CT, :Type => String field :TS, :Type => String end class PageContentSet < ContentSet field :S, :Type => Name, :Default => ContentSet::PAGE_SET, :Required => true field :T, :Type => String field :TID, :Type => String end class ImageContentSet < ContentSet field :S, :Type => Name, :Default => ContentSet::IMAGE_SET, :Required => true field :R, :Type => [ Integer, Array.of(Integer) ], :Required => true end end end origami-2.0.0/lib/origami/header.rb0000644000004100000410000000440312757133666017163 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end module Origami class PDF class InvalidHeaderError < Error #:nodoc: end # # Class representing a PDF Header. # class Header MAGIC = /%PDF-(?\d+)\.(?\d+)/ attr_accessor :major_version, :minor_version # # Creates a file header, with the given major and minor versions. # _major_version_:: Major PDF version, must be 1. # _minor_version_:: Minor PDF version, must be between 0 and 7. # def initialize(major_version = 1, minor_version = 4) @major_version, @minor_version = major_version, minor_version end def self.parse(stream) #:nodoc: unless stream.scan(MAGIC).nil? maj = stream['major'].to_i min = stream['minor'].to_i else raise InvalidHeaderError, "Invalid header format : #{stream.peek(15).inspect}" end stream.skip(REGEXP_WHITESPACES) PDF::Header.new(maj, min) end # # Outputs self into PDF code. # def to_s "%PDF-#{@major_version}.#{@minor_version}".b + EOL end def to_sym #:nodoc: "#{@major_version}.#{@minor_version}".to_sym end def to_f #:nodoc: to_sym.to_s.to_f end end end end origami-2.0.0/lib/origami/3d.rb0000644000004100000410000004155012757133666016245 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end module Origami class Projection3D < Dictionary include StandardObject ORTHOGRAPHIC = :O PERSPECTIVE = :P module ClippingStyles EXPLICIT_NEARFAR = :XNF AUTOMATIC_NEARFAR = :ANF end module Scaling WIDTH = :W HEIGHT = :H MINIMUM = :Min MAXIMUM = :Max ABSOLUTE = :Absolute end field :Subtype, :Type => Name, :Default => ORTHOGRAPHIC field :CS, :Type => Name, :Default => ClippingStyles::AUTOMATIC_NEARFAR field :F, :Type => Number field :N, :Type => Number field :FOV, :Type => Number field :PS, :Type => [ Number, Name ], :Default => Scaling::WIDTH field :OS, :Type => Number, :Default => 1 field :OB, :Type => Name, :Version => "1.7", :Default => Scaling::ABSOLUTE end class Background3D < Dictionary include StandardObject field :Type, :Type => Name, :Default => :"3DBG" field :Subtype, :Type => Name, :Default => :SC field :CS, :Type => [ Name, Array ], :Default => Graphics::Color::Space::DEVICE_RGB field :C, :Type => Object, :Default => [ 1, 1, 1 ] field :EA, :Type => Boolean, :Default => false end class RenderMode3D < Dictionary include StandardObject module Modes SOLID = :Solid SOLID_WIREFRAME = :SolidWireFrame TRANSPARENT = :Transparent TRANSPARENT_WIREFRAME = :TransparentWireFrame BOUNDINGBOX = :BoundingBox TRANSPARENT_BOUNDINGBOX = :TransparentBoundingBox TRANSPARENT_BOUNDINGBOX_OUTLINE = :TransparentBoundingBoxOutline WIREFRAME = :WireFrame SHADED_WIREFRAME = :ShadedWireFrame HIDDEN_WIREFRAME = :HiddenWireFrame VERTICES = :Vertices SHADED_VERTICES = :ShadedVertices ILLUSTRATION = :Illustration SOLID_OUTLINE = :SolidOutline SHADED_ILLUSTRATION = :ShadedIllustration end field :Type, :Type => Name, :Default => :"3DRenderMode" field :Subtype, :Type => Name, :Required => true, :Version => "1.7" field :AC, :Type => Array, :Default => [ Graphics::Color::Space::DEVICE_RGB, 0, 0, 0] field :BG, :Type => [ Name, Array ], :Default => :BG field :O, :Type => Number, :Default => 0.5 field :CV, :Type => Number, :Default => 45 end class LightingScheme3D < Dictionary include StandardObject module Styles ARTWORK = :Artwork NONE = :None WHITE = :White DAY = :Day NIGHT = :Night HARD = :Hard PRIMARY = :Primary BLUE = :Blue RED = :Red CUBE = :Cube CAD = :CAD HEADLAMP = :HeadLamp end field :Type, :Type => Name, :Default => :"3DLightingScheme" field :Subtype, :Type => Name, :Version => "1.7", :Required => true end class CrossSection3D < Dictionary include StandardObject field :Type, :Type => Name, :Default => :"3DCrossSection" field :C, :Type => Array, :Default => [ 0, 0, 0 ] field :O, :Type => Array, :Version => "1.7", :Default => [ Null.new, 0, 0 ], :Required => true field :PO, :Type => Number, :Default => 0.5 field :PC, :Type => Array, :Default => [ Graphics::Color::Space::DEVICE_RGB, 1, 1, 1 ] field :IV, :Type => Boolean, :Default => false field :IC, :Type => Array, :Default => [ Graphics::Color::Space::DEVICE_RGB, 0, 1 ,0] end class Node3D < Dictionary include StandardObject field :Type, :Type => Name, :Default => :"3DNode" field :N, :Type => String, :Version => "1.7", :Required => true field :O, :Type => Number field :V, :Type => Boolean field :M, :Type => Array end class Measurement3D < Dictionary include StandardObject field :Type, :Type => Name, :Default => :"3DMeasure" field :Subtype, :Type => Name, :Required => true field :TRL, :Type => String end class LinearDimensionMeasurement3D < Measurement3D field :Subtype, :Type => Name, :Default => :L3D, :Required => true field :AP, :Type => Array.of(Number, length: 3), :Required => true field :A1, :Type => Array.of(Number, length: 3), :Required => true field :N1, :Type => String field :A2, :Type => Array.of(Number, length: 3), :Required => true field :N2, :Type => String field :TP, :Type => Array.of(Number, length: 3), :Required => true field :TY, :Type => Array.of(Number, length: 3), :Required => true field :TS, :Type => Number field :C, :Type => Array.of(Number, length: 3) field :V, :Type => Number, :Required => true field :U, :Type => String, :Required => true field :P, :Type => Integer, :Default => 3 field :UT, :Type => String field :S, :Type => Annotation::Projection end class PerpendicularDimensionMeasurement3D < Measurement3D field :Subtype, :Type => Name, :Default => :PD3, :Required => true field :AP, :Type => Array.of(Number, length: 3), :Required => true field :A1, :Type => Array.of(Number, length: 3), :Required => true field :N1, :Type => String field :A2, :Type => Array.of(Number, length: 3), :Required => true field :N2, :Type => String field :D1, :Type => Array.of(Number, length: 3), :Required => true field :TP, :Type => Array.of(Number, length: 3), :Required => true field :TY, :Type => Array.of(Number, length: 3), :Required => true field :TS, :Type => Number field :C, :Type => Array.of(Number, length: 3) field :V, :Type => Number, :Required => true field :U, :Type => String, :Required => true field :P, :Type => Integer, :Default => 3 field :UT, :Type => String field :S, :Type => Annotation::Projection end class AngularDimensionMeasurement3D < Measurement3D field :Subtype, :Type => Name, :Default => :AD3, :Required => true field :AP, :Type => Array.of(Number, length: 3), :Required => true field :A1, :Type => Array.of(Number, length: 3), :Required => true field :D1, :Type => Array.of(Number, length: 3), :Required => true field :N1, :Type => String field :A2, :Type => Array.of(Number, length: 3), :Required => true field :D2, :Type => Array.of(Number, length: 3), :Required => true field :N2, :Type => String field :TP, :Type => Array.of(Number, length: 3), :Required => true field :TX, :Type => Array.of(Number, length: 3), :Required => true field :TY, :Type => Array.of(Number, length: 3), :Required => true field :TS, :Type => Number field :C, :Type => Array.of(Number, length: 3) field :V, :Type => Number, :Required => true field :P, :Type => Integer, :Default => 3 field :UT, :Type => String field :DR, :Type => Boolean, :Default => true field :S, :Type => Annotation::Projection end class RadialMeasurement3D < Measurement3D field :Subtype, :Type => Name, :Default => :RD3, :Required => true field :AP, :Type => Array.of(Number, length: 3), :Required => true field :A1, :Type => Array.of(Number, length: 3), :Required => true field :A2, :Type => Array.of(Number, length: 3), :Required => true field :N2, :Type => String field :A3, :Type => Array.of(Number, length: 3), :Required => true field :A4, :Type => Array.of(Number, length: 3), :Required => true field :TP, :Type => Array.of(Number, length: 3), :Required => true field :TX, :Type => Array.of(Number, length: 3), :Required => true field :TY, :Type => Array.of(Number, length: 3), :Required => true field :EL, :Type => Number, :Default => 60 field :TS, :Type => Number field :C, :Type => Array.of(Number, length: 3) field :V, :Type => Number, :Required => true field :U, :Type => String, :Required => true field :P, :Type => Integer, :Default => 3 field :UT, :Type => String field :SC, :Type => Boolean, :Default => false field :R, :Type => Boolean, :Default => true field :S, :Type => Annotation::Projection end class CommentNote3D < Measurement3D field :Subtype, :Type => Name, :Default => :"3DC", :Required => true field :A1, :Type => Array.of(Number, length: 3), :Required => true field :N1, :Type => String field :TP, :Type => Array.of(Number, length: 3), :Required => true field :TB, :Type => Array.of(Integer, length: 2) field :TS, :Type => Number field :C, :Type => Array.of(Number, length: 3) field :UT, :Type => String field :S, :Type => Annotation::Projection end class View3D < Dictionary include StandardObject field :Type, :Type => Name, :Default => :"3DView" field :XN, :Type => String, :Required => true field :IN, :Type => String field :MS, :Type => Name field :C2W, :Type => Array field :U3DPath, :Type => [ String, Array.of(String) ] field :CO, :Type => Number field :P, :Type => Projection3D field :O, :Type => Graphics::FormXObject field :BG, :Type => Background3D field :RM, :Type => RenderMode3D, :Version => "1.7" field :LS, :Type => LightingScheme3D, :Version => "1.7" field :SA, :Type => Array.of(CrossSection3D), :Version => "1.7" field :NA, :Type => Array.of(Node3D), :Version => "1.7" field :NR, :Type => Boolean, :Version => "1.7", :Default => false end class AnimationStyle3D < Dictionary include StandardObject module Styles NONE = :None LINEAR = :Linear OSCILLATING = :Oscillating end field :Type, :Type => Name, :Default => :"3DAnimationStyle" field :Subtype, :Type => Name, :Default => Styles::NONE field :PC, :Type => Integer, :Default => 0 field :TM, :Type => Number, :Default => 1 end class U3DStream < Stream include StandardObject module Type U3D = :U3D PRC = :PRC end field :Type, :Type => Name, :Default => :"3D" field :Subtype, :Type => Name, :Default => Type::U3D, :Required => true, :Version => "1.7", :ExtensionLevel => 3 field :VA, :Type => Array.of(View3D) field :DV, :Type => Object field :Resources, :Type => Dictionary field :OnInstantiate, :Type => Stream field :AN, :Type => AnimationStyle3D def onInstantiate(action) self[:OnInstantiate] = action end end class Reference3D < Dictionary include StandardObject field :Type, :Type => Name, :Default => :"3DRef" field :"3DD", :Type => U3DStream end class Units3D < Dictionary include StandardObject field :TSm, :Type => Number, :Default => 1.0 field :TSn, :Type => Number, :Default => 1.0 field :TU, :Type => String field :USm, :Type => Number, :Default => 1.0 field :USn, :Type => Number, :Default => 1.0 field :UU, :Type => String field :DSm, :Type => Number, :Default => 1.0 field :DSn, :Type => Number, :Default => 1.0 field :DU, :Type => String end class Annotation # # 3D Artwork annotation. # class Artwork3D < Annotation class Activation < Dictionary include StandardObject module Events PAGE_OPEN = :PO PAGE_CLOSE = :PC PAGE_VISIBLE = :PV PAGE_INVISIBLE = :PI USER_ACTIVATE = :XA USER_DEACTIVATE = :XD end module State UNINSTANCIATED = :U INSTANCIATED = :I LIVE = :L end module Style EMBEDDED = :Embedded WINDOWED = :Windowed end field :A, :Type => Name, :Default => Events::USER_ACTIVATE field :AIS, :Type => Name, :Default => State::LIVE field :D, :Type => Name, :Default => Events::PAGE_INVISIBLE field :DIS, :Type => Name, :Default => State::UNINSTANCIATED field :TB, :Type => Boolean, :Version => "1.7", :Default => true field :NP, :Type => Boolean, :Version => "1.7", :Default => false field :Style, :Type => Name, :Version => "1.7", :ExtensionLevel => 3, :Default => Style::EMBEDDED field :Window, :Type => RichMedia::Window, :Version => "1.7", :ExtensionLevel => 3 field :Transparent, :Type => Boolean, :Version => "1.7", :ExtensionLevel => 3, :Default => false end field :Subtype, :Type => Name, :Default => :"3D", :Version => "1.6", :Required => true field :"3DD", :Type => [ Reference3D, U3DStream ], :Required => true field :"3DV", :Type => Object field :"3DA", :Type => Activation field :"3DI", :Type => Boolean, :Default => true field :"3DB", :Type => Rectangle field :"3DU", :Type => Units3D, :Version => "1.7", :ExtensionLevel => 3 end end end origami-2.0.0/lib/origami/acroform.rb0000644000004100000410000002262612757133666017552 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end module Origami class PDF # # Returns true if the document contains an acrobat form. # def form? (not self.Catalog.nil?) and self.Catalog.has_key? :AcroForm end # # Creates a new AcroForm with specified fields. # def create_form(*fields) acroform = self.Catalog.AcroForm ||= InteractiveForm.new.set_indirect(true) self.add_fields(*fields) acroform end # # Add a field to the Acrobat form. # _field_:: The Field to add. # def add_fields(*fields) raise TypeError, "Expected Field arguments" unless fields.all? { |f| f.is_a?(Field) } self.Catalog.AcroForm ||= InteractiveForm.new.set_indirect(true) self.Catalog.AcroForm.Fields ||= [] self.Catalog.AcroForm.Fields.concat(fields) fields.each do |field| field.set_indirect(true) end self end # # Returns an Array of Acroform fields. # def fields self.each_field.to_a end # # Iterates over each Acroform Field. # def each_field return enum_for(__method__) do if self.form? and self.Catalog.AcroForm[:Fields].is_a?(Array) self.Catalog.AcroForm[:Fields].length else 0 end end unless block_given? if self.form? and self.Catalog.AcroForm[:Fields].is_a?(Array) self.Catalog.AcroForm[:Fields].each do |field| yield(field.solve) end end end # # Returns the corresponding named Field. # def get_field(name) self.each_field do |field| return field if field[:T].solve == name end end end # # Class representing a interactive form Dictionary. # class InteractiveForm < Dictionary include StandardObject # # Flags relative to signature fields. # module SigFlags SIGNATURESEXIST = 1 << 0 APPENDONLY = 1 << 1 end field :Fields, :Type => Array, :Required => true, :Default => [] field :NeedAppearances, :Type => Boolean, :Default => false field :SigFlags, :Type => Integer, :Default => 0 field :CO, :Type => Array, :Version => "1.3" field :DR, :Type => Dictionary field :DA, :Type => String field :Q, :Type => Integer field :XFA, :Type => [ XFAStream, Array.of(String, XFAStream) ] end module Field # # Types of fields. # module Type BUTTON = :Btn TEXT = :Tx CHOICE = :Ch SIGNATURE = :Sig end # # Flags relative to fields. # module Flags READONLY = 1 << 0 REQUIRED = 1 << 1 NOEXPORT = 1 << 2 end module TextAlign LEFT = 0 CENTER = 1 RIGHT = 2 end class AdditionalActions < Dictionary include StandardObject field :K, :Type => Dictionary, :Version => "1.3" field :F, :Type => Dictionary, :Version => "1.3" field :V, :Type => Dictionary, :Version => "1.3" field :C, :Type => Dictionary, :Version => "1.3" end def self.included(receiver) #:nodoc: receiver.field :FT, :Type => Name, :Required => true receiver.field :Parent, :Type => Field receiver.field :Kids, :Type => Array.of(Field) receiver.field :T, :Type => String receiver.field :TU, :Type => String, :Version => "1.3" receiver.field :TM, :Type => String, :Version => "1.3" receiver.field :Ff, :Type => Integer, :Default => 0 receiver.field :V, :Type => Object receiver.field :DV, :Type => Object receiver.field :AA, :Type => AdditionalActions, :Version => "1.2" # Variable text fields receiver.field :DA, :Type => String, :Default => "/F1 10 Tf 0 g", :Required => true receiver.field :Q, :Type => Integer, :Default => TextAlign::LEFT receiver.field :DS, :Type => String, :Version => "1.5" receiver.field :RV, :Type => [ String, Stream ], :Version => "1.5" end def pre_build #:nodoc: self.T ||= "undef#{::Array.new(5) {('0'.ord + rand(10)).chr}.join}" super end def onKeyStroke(action) unless action.is_a?(Action) raise TypeError, "An Action object must be passed." end self.AA ||= AdditionalActions.new self.AA.K = action end def onFormat(action) unless action.is_a?(Action) raise TypeError, "An Action object must be passed." end self.AA ||= AdditionalActions.new self.AA.F = action end def onValidate(action) unless action.is_a?(Action) raise TypeError, "An Action object must be passed." end self.AA ||= AdditionalActions.new self.AA.V = action end def onCalculate(action) unless action.is_a?(Action) raise TypeError, "An Action object must be passed." end self.AA ||= AdditionalActions.new self.AA.C = action end class Subform < Dictionary include StandardObject include Field def add_fields(*fields) self.Kids ||= [] self.Kids.concat(fields) fields.each { |field| field.Parent = self } self end end class SignatureLock < Dictionary include StandardObject module Actions ALL = :All INCLUDE = :Include EXCLUDE = :Exclude end field :Type, :Type => Name, :Default => :SigFieldLock field :Action, :Type => Name, :Required => true field :Fields, :Type => Array def pre_build if self.Action and self.Action != Actions::ALL self.Fields ||= [] end super end end class SignatureSeedValue < Dictionary include StandardObject module Digest SHA1 = :SHA1 SHA256 = :SHA256 SHA384 = :SHA384 SHA512 = :SHA512 RIPEMD160 = :RIPEMD160 end field :Type, :Type => Name, :Default => :SV field :Filter, :Type => Name field :SubFilter, :Type => Array field :DigestMethod, :Type => Array, :Default => Digest::SHA1, :Version => "1.7" field :V, :Type => Real, :Default => 1.0 field :Cert, :Type => Dictionary field :Reasons, :Type => Array field :MDP, :Type => Dictionary, :Version => "1.6" field :TimeStamp, :Type => Dictionary, :Version => "1.6" field :LegalAttestation, :Type => Array, :Version => "1.6" field :AddRevInfo, :Type => Boolean, :Default => false, :Version => "1.7" field :Ff, :Type => Integer, :Default => 0 end class CertificateSeedValue < Dictionary include StandardObject module URL BROWSER = :Browser ASSP = :ASSP end field :Type, :Type => Name, :Default => :SVCert field :Subject, :Type => Array field :SubjectDN, :Type => Array, :Version => "1.7" field :KeyUsage, :Type => Array, :Version => "1.7" field :Issuer, :Type => Array field :OID, :Type => Array field :URL, :Type => String field :URLType, :Type => Name, :Default => URL::BROWSER, :Version => "1.7" field :Ff, :Type => Integer, :Default => 0 end end end origami-2.0.0/lib/origami/outputintents.rb0000644000004100000410000000574312757133666020710 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end module Origami class OutputIntent < Dictionary include StandardObject module Intent PDFX = :GTS_PDFX PDFA1 = :GTS_PDFA1 PDFE1 = :GTS_PDFE1 end field :Type, :Type => Name, :Default => :OutputIntent field :S, :Type => Name, :Version => '1.4', :Required => true field :OutputCondition, :Type => String field :OutputConditionIdentifier, :Type => String field :RegistryName, :Type => String field :Info, :Type => String field :DestOutputProfile, :Type => Stream end class PDF def pdfa1? self.Catalog.OutputIntents.is_a?(Array) and self.Catalog.OutputIntents.any?{|intent| intent.solve.S == OutputIntent::Intent::PDFA1 } and self.metadata? and ( doc = REXML::Document.new self.Catalog.Metadata.data; REXML::XPath.match(doc, "*/*/rdf:Description[@xmlns:pdfaid]").any? {|desc| desc.elements["pdfaid:conformance"].text == "A" and desc.elements["pdfaid:part"].text == "1" } ) end private def intents_as_pdfa1 return if self.pdfa1? self.Catalog.OutputIntents ||= [] self.Catalog.OutputIntents << self.insert( OutputIntent.new( :Type => :OutputIntent, :S => OutputIntent::Intent::PDFA1, :OutputConditionIdentifier => "RGB" ) ) metadata = self.create_metadata doc = REXML::Document.new(metadata.data) desc = REXML::Element.new 'rdf:Description' desc.add_attribute 'rdf:about', '' desc.add_attribute 'xmlns:pdfaid', 'http://www.aiim.org/pdfa/ns/id/' desc.add REXML::Element.new('pdfaid:conformance').add_text('A') desc.add REXML::Element.new('pdfaid:part').add_text('1') doc.elements["*/rdf:RDF"].add desc xml = ""; doc.write(xml, 3) metadata.data = xml end end end origami-2.0.0/lib/origami/boolean.rb0000644000004100000410000000446212757133666017357 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end module Origami class InvalidBooleanObjectError < InvalidObjectError #:nodoc: end # # Class representing a Boolean Object. # A Boolean Object can be *true* or *false*. # class Boolean include Origami::Object TOKENS = %w{ true false } #:nodoc: @@regexp = Regexp.new(WHITESPACES + "(?#{Regexp.union(TOKENS)})") # # Creates a new Boolean value. # _value_:: *true* or *false*. # def initialize(value) unless value.is_a?(TrueClass) or value.is_a?(FalseClass) raise TypeError, "Expected type TrueClass or FalseClass, received #{value.class}." end super() @value = (value == true) end def to_s #:nodoc: super(@value.to_s) end def self.parse(stream, parser = nil) #:nodoc: offset = stream.pos if stream.scan(@@regexp).nil? raise InvalidBooleanObjectError end value = (stream['value'] == "true") bool = Boolean.new(value) bool.file_offset = offset bool end # # Converts self into a Ruby boolean, that is TrueClass or FalseClass instance. # def value @value end def self.native_type ; Boolean end def false? @value == false end def true? @value == true end def ==(bool) @value == bool end end end origami-2.0.0/lib/origami/version.rb0000644000004100000410000000147712757133666017430 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end module Origami VERSION = "2.0.0" end origami-2.0.0/lib/origami/trailer.rb0000644000004100000410000001141312757133666017374 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end module Origami class PDF private def trailer_key?(attr) #:nodoc: !!trailer_key(attr) end def trailer_key(attr) #:nodoc: @revisions.reverse_each do |rev| if rev.trailer.has_dictionary? and not rev.trailer[attr].nil? return rev.trailer[attr].solve elsif rev.has_xrefstm? xrefstm = rev.xrefstm if xrefstm.is_a?(XRefStream) and xrefstm.has_field?(attr) return xrefstm[attr].solve end end end nil end def get_trailer_info #:nodoc: # # First look for a standard trailer dictionary # if @revisions.last.trailer.has_dictionary? @revisions.last.trailer # # Otherwise look for a xref stream. # else @revisions.last.xrefstm end end def generate_id info = get_trailer_info if info.nil? raise InvalidPDFError, "Cannot access trailer information" end id = HexaString.new Random.new.bytes 16 info.ID = [ id, id ] end end class InvalidTrailerError < Error #:nodoc: end # Forward declarations. class Catalog < Dictionary; end class Metadata < Dictionary; end # # Class representing a PDF file Trailer. # class Trailer include StandardObject TOKENS = %w{ trailer %%EOF } #:nodoc: XREF_TOKEN = "startxref" #:nodoc: @@regexp_open = Regexp.new(WHITESPACES + TOKENS.first + WHITESPACES) @@regexp_xref = Regexp.new(WHITESPACES + XREF_TOKEN + WHITESPACES + "(?\\d+)") @@regexp_close = Regexp.new(WHITESPACES + TOKENS.last + WHITESPACES) attr_accessor :document attr_accessor :startxref attr_reader :dictionary field :Size, :Type => Integer, :Required => true field :Prev, :Type => Integer field :Root, :Type => Catalog, :Required => true field :Encrypt, :Type => Encryption::Standard::Dictionary field :Info, :Type => Metadata field :ID, :Type => Array.of(String, length: 2) field :XRefStm, :Type => Integer # # Creates a new Trailer. # _startxref_:: The file _offset_ to the XRef::Section. # _dictionary_:: A hash of attributes to set in the Trailer Dictionary. # def initialize(startxref = 0, dictionary = {}) @startxref, self.dictionary = startxref, dictionary && Dictionary.new(dictionary) end def self.parse(stream, parser = nil) #:nodoc: if stream.skip(@@regexp_open) dictionary = Dictionary.parse(stream, parser) else dictionary = nil end if not stream.scan(@@regexp_xref) #raise InvalidTrailerError, "Cannot get startxref value" end startxref = stream['startxref'].to_i if not stream.scan(@@regexp_close) #raise InvalidTrailerError, "No %%EOF token found" end Trailer.new(startxref, dictionary) end def [](key) @dictionary[key] if has_dictionary? end def []=(key,val) @dictionary[key] = val end def dictionary=(dict) dict.parent = self if dict @dictionary = dict end def has_dictionary? not @dictionary.nil? end # # Outputs self into PDF code. # def to_s content = "" if self.has_dictionary? content << TOKENS.first << EOL << @dictionary.to_s << EOL end content << XREF_TOKEN << EOL << @startxref.to_s << EOL << TOKENS.last << EOL content end end end origami-2.0.0/lib/origami/array.rb0000644000004100000410000002220712757133666017053 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end module Origami class InvalidArrayObjectError < InvalidObjectError #:nodoc: end # # Class representing an Array Object. # Arrays contain a set of Object. # class Array < ::Array include Origami::Object using TypeConversion TOKENS = %w{ [ ] } #:nodoc: @@regexp_open = Regexp.new(WHITESPACES + Regexp.escape(TOKENS.first) + WHITESPACES) @@regexp_close = Regexp.new(WHITESPACES + Regexp.escape(TOKENS.last) + WHITESPACES) attr_reader :strings_cache, :names_cache, :xref_cache # # Creates a new PDF Array Object. # _data_:: An array of objects. # def initialize(data = [], parser = nil, hint_type: nil) raise TypeError, "Expected type Array, received #{data.class}." unless data.is_a?(::Array) super() @strings_cache = [] @names_cache = [] @xref_cache = {} data.each_with_index do |value, index| value = value.to_o if Origami::OPTIONS[:enable_type_guessing] index_type = hint_type.is_a?(::Array) ? hint_type[index % hint_type.size] : hint_type if index_type.is_a?(::Array) and not value.is_a?(Reference) index_type = index_type.find {|type| type < value.class } end if index_type.is_a?(Class) and index_type < value.class value = value.cast_to(index_type, parser) end if index_type and parser and Origami::OPTIONS[:enable_type_propagation] if value.is_a?(Reference) parser.defer_type_cast(value, index_type) end end end # Cache object value for fast search. cache_value(value) self.push(value) end end def pre_build self.map!{|obj| obj.to_o} super end def self.parse(stream, parser = nil, hint_type: nil) #:nodoc: data = [] offset = stream.pos if not stream.skip(@@regexp_open) raise InvalidArrayObjectError, "No token '#{TOKENS.first}' found" end while stream.skip(@@regexp_close).nil? do type = Object.typeof(stream) raise InvalidArrayObjectError, "Bad embedded object format" if type.nil? value = type.parse(stream, parser) data << value end array = Array.new(data, parser, hint_type: hint_type) array.file_offset = offset array end # # Converts self into a Ruby array. # def to_a super.map(&:value) end alias value to_a def to_s #:nodoc: content = "#{TOKENS.first} " self.each do |entry| entry = entry.to_o case entry when Dictionary # Do not indent dictionaries inside of arrays. content << entry.to_s(indent: 0) << ' ' else content << entry.to_s << ' ' end end content << TOKENS.last super(content) end def +(other) a = Origami::Array.new(self.to_a + other.to_a) a.no, a.generation = @no, @generation a end def <<(item) obj = item.to_o obj.parent = self unless obj.indirect? super(obj) end alias push << def []=(key,val) key, val = key.to_o, val.to_o super(key.to_o, val.to_o) val.parent = self unless val.indirect? or val.parent.equal?(self) val end def copy copy = self.class.new self.each do |obj| copy << obj.copy end copy.parent = @parent copy.no, copy.generation = @no, @generation copy.set_indirect(true) if self.indirect? copy.set_document(@document) if self.indirect? copy end def cast_to(type, parser = nil) super(type) cast = type.new(self, parser) cast.parent = self.parent cast.no, cast.generation = self.no, self.generation if self.indirect? cast.set_indirect(true) cast.set_document(self.document) cast.file_offset = self.file_offset # cast can replace self end cast end def self.native_type ; Origami::Array end # # Parameterized Array class with additional typing information. # Example: Array.of(Integer) # def self.of(klass, *klasses, length: nil) Class.new(self) do const_set('ARRAY_TYPE', (klasses.empty? and not klass.is_a?(::Array)) ? klass : [ klass ].concat(klasses)) const_set('STATIC_LENGTH', length) def initialize(data = [], parser = nil) super(data, parser, hint_type: self.class.const_get('ARRAY_TYPE')) end def pre_build #:nodoc: do_type_check if Origami::OPTIONS[:enable_type_checking] super end def self.parse(stream, parser = nil) super(stream, parser, hint_type: const_get('ARRAY_TYPE')) end def do_type_check #:nodoc: static_length = self.class.const_get('STATIC_LENGTH') array_type = self.class.const_get('ARRAY_TYPE') if static_length and self.length != static_length STDERR.puts "Warning: object #{self.class.name} has unexpected length #{self.length} (should be #{static_length})" end self.each_with_index do |object, index| index_type = array_type.is_a?(::Array) ? array_type[index % array_type.size] : array_type begin object_value = object.solve rescue InvalidReferenceError STDERR.puts "Warning: in object #{self.class}, invalid reference at index #{index}" next end unless object_value.is_a?(index_type) STDERR.puts "Warning: object #{self.class.name || 'Array'} should be composed of #{index_type.name} at index #{index} (got #{object_value.type} instead)" end end end end end private def cache_value(value) case value when String then @strings_cache.push(value) when Name then @names_cache.push(value) when Reference then (@xref_cache[value] ||= []).push(self) when Dictionary, Array @strings_cache.concat(value.strings_cache) @names_cache.concat(value.names_cache) @xref_cache.update(value.xref_cache) do |_ref, cache1, cache2| cache1.concat(cache2) end value.strings_cache.clear value.names_cache.clear value.xref_cache.clear end end end # # Class representing a location on a page or a bounding box. # class Rectangle < Array.of(Number, length: 4) def self.[](coords) corners = if [ :llx, :lly, :urx, :ury ].all? {|p| coords.include?(p)} coords.values_at(:llx, :lly, :urx, :ury) elsif [ :width, :height ].all? {|p| coords.include?(p)} width, height = coords.values_at(:width, :height) x = coords.fetch(:x, 0) y = coords.fetch(:y, 0) [ x, y, x+width, y+height ] else raise ArgumentError, "Bad arguments for #{self.class}: #{coords.inspect}" end unless corners.all? { |corner| corner.is_a?(Numeric) } raise TypeError, "All coords must be numbers" end Rectangle.new(corners) end end end origami-2.0.0/lib/origami/actions.rb0000644000004100000410000002511712757133666017400 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end module Origami class PDF # # Lookup script in the scripts name directory. # def get_script_by_name(name) resolve_name Names::JAVASCRIPT, name end # # Calls block for each named JavaScript script. # def each_named_script(&b) each_name(Names::JAVASCRIPT, &b) end end # # Class representing an action to launch in a PDF. # class Action < Dictionary include StandardObject field :Type, :Type => Name, :Default => :Action field :S, :Type => Name, :Required => true field :Next, :Type => [ Array.of(Action), Action ], :Version => "1.2" # # Class representing a action going to a destination in the current document. # class GoTo < Action field :S, :Type => Name, :Default => :GoTo, :Required => true field :D, :Type => [ Destination, Name, String ], :Required => true # # Creates a new GoTo Action. # _hash_:: A hash of options to set for this jump. # def self.[](hash = {}) if hash.is_a? Destination self.new(:S => :GoTo, :D => hash) else self.new(hash) end end end def self.GoTo(hash = {}) Action::GoTo[hash] end # # Class representing an action launching an URL. # class URI < Action field :S, :Type => Name, :Default => :URI, :Required => true field :URI, :Type => String, :Required => true field :IsMap, :Type => Boolean, :Default => false # # Creates a new URI Action. # _uri_:: The URI to launch. # _ismap_:: # def self.[](uri, ismap = false) self.new(:URI => uri, :IsMap => ismap) end end def self.URI(uri, ismap = false) Action::URI[uri, ismap] end # # Class representing a JavaScript Action. # class JavaScript < Action field :S, :Type => Name, :Default => :JavaScript, :Required => true field :JS, :Type => [ Stream, String ], :Required => true # # Creates a new JavaScript Action. # _script_:: The script to be executed. # def self.[](script) self.new(:JS => script) end end def self.JavaScript(script) Action::JavaScript[script] end # # Class representing an Action which run a command on the current system. # class Launch < Action # # Dictionary for passing parameter to Windows applications during Launch. # class WindowsLaunchParams < Dictionary include StandardObject field :F, :Type => String, :Required => true field :D, :Type => String field :O, :Type => String, :Default => "open" field :P, :Type => String end field :S, :Type => Name, :Default => :Launch, :Required => true field :F, :Type => [ String, FileSpec ] field :Win, :Type => WindowsLaunchParams field :Mac, :Type => Object field :Unix, :Type => Object field :NewWindow, :Type => Boolean end # # Class representing a Named Action. # Named actions are predefined GoTo actions. # class Named < Action field :S, :Type => Name, :Default => :Named, :Required => true field :N, :Type => Name, :Required => true def self.[](type) self.new(:N => type) end NEXT_PAGE = self[:NextPage] PREV_PAGE = self[:PrevPage] FIRST_PAGE = self[:FirstPage] LAST_PAGE = self[:LastPage] PRINT = self[:Print] end def self.Named(type) Action::Named[type] end # # Class representing a GoTo Action to an external file. # class GoToR < Action field :S, :Type => Name, :Default => :GoToR, :Required => true field :F, :Type => [ String, FileSpec ], :Required => true field :D, :Type => [ Destination, Name, String ], :Required => true field :NewWindow, :Type => Boolean, :Version => "1.2" # # Creates a new GoTo remote Action. # _file_:: A FileSpec describing the file. # _dest_:: A Destination in the file. # _new_window_:: Specifies whether the file has to be opened in a new window. # def self.[](file, dest: Destination::GlobalFit[0], new_window: false) self.new(:F => file, :D => dest, :NewWindow => new_window) end end def self.GoToR(file, dest: Destination::GlobalFit[0], new_window: false) Action::GoToR[file, dest: dest, new_window: new_window] end # # Class representing a GoTo Action to an embedded pdf file. # class GoToE < Action # # A class representing a target for a GoToE to an embedded file. # class EmbeddedTarget < Dictionary include StandardObject module Relationship PARENT = :P CHILD = :C end field :R, :Type => Name, :Required => true field :N, :Type => String field :P, :Type => [ Integer, String ] field :A, :Type => [ Integer, String ] field :T, :Type => Dictionary end field :S, :Type => Name, :Default => :GoToE, :Required => true field :F, :Type => [ String, FileSpec ] field :D, :Type => [ Destination, Name, String ], :Required => true field :NewWindow, :Type => Boolean field :T, :Type => EmbeddedTarget def self.[](filename, dest: Destination::GlobalFit[0], new_window: false) self.new(:T => EmbeddedTarget.new(:R => :C, :N => filename), :D => dest, :NewWindow => new_window) end end def self.GoToE(filename, dest: Destination::GlobalFit[0], new_window: false) Action::GoToE[filename, dest: dest, new_window: new_window] end # # Class representing a SubmitForm action. # class SubmitForm < Action module Flags INCLUDEEXCLUDE = 1 << 0 INCLUDENOVALUEFIELDS = 1 << 1 EXPORTFORMAT = 1 << 2 GETMETHOD = 1 << 3 SUBMITCOORDINATES = 1 << 4 XFDF = 1 << 5 INCLUDEAPPENDSAVES = 1 << 6 INCLUDEANNOTATIONS = 1 << 7 SUBMITPDF = 1 << 8 CANONICALFORMAT = 1 << 9 EXCLNONUSERANNOTS = 1 << 10 EXCLFKEY = 1 << 11 EMBEDFORM = 1 << 12 end field :S, :Type => Name, :Default => :SubmitForm, :Required => true field :F, :Type => FileSpec field :Fields, :Type => Array field :Flags, :Type => Integer, :Default => 0 def self.[](url, fields = [], flags = 0) url = FileSpec.new(:FS => :URL, :F => url) unless url.is_a? FileSpec self.new(:F => url, :Fields => fields, :Flags => flags) end end def self.SubmitForm(url, fields = [], flags = 0) Action::SubmitForm[url, fields, flags] end class ImportData < Action field :S, :Type => Name, :Default => :ImportData, :Required => true field :F, :Type => Dictionary, :Required => true def self.[](file) file = FileSpec.new(:FS => :File, :F => file) unless file.is_a? FileSpec self.new(:F => file) end end def self.ImportData(file) Action::ImportData[file] end class RichMediaExecute < Action class Command < Dictionary include StandardObject field :Type, :Type => Name, :Default => :RichMediaCommand, :Version => "1.7", :ExtensionLevel => 3 field :C, :Type => String, :Version => "1.7", :ExtensionLevel => 3, :Required => true field :A, :Type => Object, :Version => "1.7", :ExtensionLevel => 3 end field :S, :Type => Name, :Default => :RichMediaExecute, :Version => "1.7", :ExtensionLevel => 3, :Required => true field :TA, :Type => Annotation::RichMedia, :Version => "1.7", :ExtensionLevel => 3, :Required => true field :TI, :Type => Annotation::RichMedia::Instance, :Version => "1.7", :ExtensionLevel => 3 field :CMD, :Type => Command, :Version => "1.7", :ExtensionLevel => 3, :Required => true def self.[](annotation, command, *params) self.new(:TA => annotation, :CMD => Command.new(:C => command, :A => params)) end end def self.RichMediaExecute(annotation, command, *params) Action::RichMediaExecute[annotation, command, *params] end end end origami-2.0.0/lib/origami/linearization.rb0000644000004100000410000002530712757133666020611 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end module Origami class PDF class LinearizationError < Error #:nodoc: end # # Returns whether the current document is linearized. # def linearized? begin first_obj = @revisions.first.objects.min_by{|obj| obj.file_offset} rescue return false end @revisions.size > 1 and first_obj.is_a?(Dictionary) and first_obj.has_key? :Linearized end # # Tries to delinearize the document if it has been linearized. # This operation is xrefs destructive, should be fixed in the future to merge tables. # def delinearize! raise LinearizationError, 'Not a linearized document' unless self.linearized? # # Saves the first trailer. # prev_trailer = @revisions.first.trailer lin_dict = @revisions.first.objects.min_by{|obj| obj.file_offset} hints = lin_dict[:H] # # Removes hint streams used by linearization. # if hints.is_a?(::Array) if hints.length > 0 and hints[0].is_a?(Integer) hint_stream = get_object_by_offset(hints[0]) delete_object(hint_stream.reference) if hint_stream.is_a?(Stream) end if hints.length > 2 and hints[2].is_a?(Integer) overflow_stream = get_object_by_offset(hints[2]) delete_object(overflow_stream.reference) if overflow_stream.is_a?(Stream) end end # # Update the trailer. # last_trailer = (@revisions.last.trailer ||= Trailer.new) last_trailer.dictionary ||= Dictionary.new if prev_trailer.has_dictionary? last_trailer.dictionary = last_trailer.dictionary.merge(prev_trailer.dictionary) else xrefstm = @revisions.last.xrefstm raise LinearizationError, 'Cannot find trailer info while delinearizing document' unless xrefstm.is_a?(XRefStream) last_trailer.dictionary[:Root] = xrefstm[:Root] last_trailer.dictionary[:Encrypt] = xrefstm[:Encrypt] last_trailer.dictionary[:Info] = xrefstm[:Info] last_trailer.dictionary[:ID] = xrefstm[:ID] end # # Remove all xrefs. # Fix: Should be merged instead. # remove_xrefs # # Remove the linearization revision. # @revisions.first.body.delete(lin_dict.reference) @revisions.last.body.merge! @revisions.first.body remove_revision(0) self end end # # Class representing a linearization dictionary. # class Linearization < Dictionary include StandardObject field :Linearized, :Type => Real, :Default => 1.0, :Required => true field :L, :Type => Integer, :Required => true field :H, :Type => Array.of(Integer), :Required => true field :O, :Type => Integer, :Required => true field :E, :Type => Integer, :Required => true field :N, :Type => Integer, :Required => true field :T, :Type => Integer, :Required => true field :P, :Type => Integer, :Default => 0 def initialize(hash = {}, parser = nil) super(hash, parser) set_indirect(true) end end class InvalidHintTableError < Error #:nodoc: end module HintTable module ClassMethods def header_item_size(number, size) @header_items_size[number] = size end def get_header_item_size(number) @header_items_size[number] end def entry_item_size(number, size) @entry_items_size[number] = size end def get_entry_item_size(number) @entry_items_size[number] end def nb_header_items @header_items_size.size end def nb_entry_items @entry_items_size.size end end def self.included(receiver) receiver.instance_variable_set(:@header_items_size, {}) receiver.instance_variable_set(:@entry_items_size, {}) receiver.extend(ClassMethods) end attr_accessor :header_items attr_accessor :entries def initialize @header_items = {} @entries = [] end def to_s data = "" nitems = self.class.nb_header_items for no in (1..nitems) unless @header_items.include?(no) raise InvalidHintTableError, "Missing item #{no} in header section of #{self.class}" end value = @header_items[no] item_size = self.class.get_header_item_size(no) item_size = ((item_size + 7) >> 3) << 3 item_data = value.to_s(2) item_data = "0" * (item_size - item_data.size) + item_data data << [ item_data ].pack("B*") end i = 0 nitems = self.class.nb_entry_items @entries.each do |entry| for no in (1..items) unless entry.include?(no) raise InvalidHintTableError, "Missing item #{no} in entry #{i} of #{self.class}" end value = entry[no] item_size = self.class.get_entry_item_size(no) item_size = ((item_size + 7) >> 3) << 3 item_data = value.to_s(2) item_data = "0" * (item_size - item_data.size) + item_data data << [ item_data ].pack("B*") end i = i + 1 end data end class PageOffsetTable include HintTable header_item_size 1, 32 header_item_size 2, 32 header_item_size 3, 16 header_item_size 4, 32 header_item_size 5, 16 header_item_size 6, 32 header_item_size 7, 16 header_item_size 8, 32 header_item_size 9, 16 header_item_size 10, 16 header_item_size 11, 16 header_item_size 12, 16 header_item_size 13, 16 entry_item_size 1, 16 entry_item_size 2, 16 entry_item_size 3, 16 entry_item_size 4, 16 entry_item_size 5, 16 entry_item_size 6, 16 entry_item_size 7, 16 end class SharedObjectTable include HintTable header_item_size 1, 32 header_item_size 2, 32 header_item_size 3, 32 header_item_size 4, 32 header_item_size 5, 16 header_item_size 6, 32 header_item_size 7, 16 entry_item_size 1, 16 entry_item_size 2, 1 entry_item_size 3, 128 entry_item_size 4, 16 end end class InvalidHintStreamObjectError < InvalidStreamObjectError #:nodoc: end class HintStream < Stream attr_accessor :page_offset_table attr_accessor :shared_objects_table attr_accessor :thumbnails_table attr_accessor :outlines_table attr_accessor :threads_table attr_accessor :named_destinations_table attr_accessor :interactive_forms_table attr_accessor :information_dictionary_table attr_accessor :logical_structure_table attr_accessor :page_labels_table attr_accessor :renditions_table attr_accessor :embedded_files_table field :S, :Type => Integer, :Required => true # Shared objects field :T, :Type => Integer # Thumbnails field :O, :Type => Integer # Outlines field :A, :Type => Integer # Threads field :E, :Type => Integer # Named destinations field :V, :Type => Integer # Interactive forms field :I, :Type => Integer # Information dictionary field :C, :Type => Integer # Logical structure field :L, :Type => Integer # Page labels field :R, :Type => Integer # Renditions field :B, :Type => Integer # Embedded files def pre_build if @page_offset_table.nil? raise InvalidHintStreamObjectError, "No page offset hint table" end if @shared_objects_table.nil? raise InvalidHintStreamObjectError, "No shared objects hint table" end @data = "" save_table(@page_offset_table) save_table(@shared_objects_table, :S) save_table(@thumbnails_table, :T) save_table(@outlines_table, :O) save_table(@threads_table, :A) save_table(@named_destinations_table, :E) save_table(@interactive_forms_table, :V) save_table(@information_dictionary_table, :I) save_table(@logical_structure_table, :C) save_table(@page_labels_table, :L) save_table(@renditions_table, :R) save_table(@embedded_files_table, :B) super end private def save_table(table, name = nil) unless table.nil? self[name] = @data.size if name @data << table.to_s end end end end origami-2.0.0/lib/origami/functions.rb0000644000004100000410000000604712757133666017751 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end module Origami module Function module Type SAMPLED = 0 EXPONENTIAL = 2 STITCHING = 3 POSTSCRIPT = 4 end def self.included(receiver) receiver.field :FunctionType, :Type => Integer, :Required => true receiver.field :Domain, :Type => Array.of(Number), :Required => true receiver.field :Range, :Type => Array.of(Number) end class Sampled < Stream include Function field :FunctionType, :Type => Integer, :Default => Type::SAMPLED, :Version => "1.3", :Required => true field :Range, :Type => Array.of(Number), :Required => true field :Size, :Type => Array.of(Integer), :Required => true field :BitsPerSample, :Type => Integer, :Required => true field :Order, :Type => Integer, :Default => 1 field :Encode, :Type => Array.of(Number) field :Decode, :Type => Array.of(Number) end class Exponential < Dictionary include StandardObject include Function field :FunctionType, :Type => Integer, :Default => Type::EXPONENTIAL, :Version => "1.3", :Required => true field :C0, :Type => Array.of(Number), :Default => [ 0.0 ] field :C1, :Type => Array.of(Number), :Default => [ 1.0 ] field :N, :Type => Number, :Required => true end class Stitching < Dictionary include StandardObject include Function field :FunctionType, :Type => Integer, :Default => Type::STITCHING, :Version => "1.3", :Required => true field :Functions, :Type => Array, :Required => true field :Bounds, :Type => Array.of(Number), :Required => true field :Encode, :Type => Array.of(Number), :Required => true end class PostScript < Stream include Function field :FunctionType, :Type => Integer, :Default => Type::POSTSCRIPT, :Version => "1.3", :Required => true field :Range, :Type => Array.of(Number), :Required => true end end end origami-2.0.0/lib/origami/graphics/0000755000004100000410000000000012757133666017205 5ustar www-datawww-dataorigami-2.0.0/lib/origami/graphics/instruction.rb0000644000004100000410000000622012757133666022113 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end module Origami class InvalidPDFInstructionError < Error; end class PDF::Instruction using TypeConversion attr_reader :operator attr_accessor :operands @insns = Hash.new(operands: [], render: lambda{}) def initialize(operator, *operands) @operator = operator @operands = operands.map!{|arg| arg.is_a?(Origami::Object) ? arg.value : arg} if self.class.has_op?(operator) opdef = self.class.get_operands(operator) if not opdef.include?('*') and opdef.size != operands.size raise InvalidPDFInstructionError, "Numbers of operands mismatch for #{operator}: #{operands.inspect}" end end end def render(canvas) self.class.get_render_proc(@operator)[canvas, *@operands] self end def to_s "#{operands.map{|op| op.to_o.to_s}.join(' ')}#{' ' unless operands.empty?}#{operator}\n" end class << self def insn(operator, *operands, &render_proc) @insns[operator] = {} @insns[operator][:operands] = operands @insns[operator][:render] = render_proc || lambda{} end def has_op?(operator) @insns.has_key? operator end def get_render_proc(operator) @insns[operator][:render] end def get_operands(operator) @insns[operator][:operands] end def parse(stream) operands = [] while type = Object.typeof(stream, true) operands.push type.parse(stream) end if not stream.eos? if stream.scan(/(?[[:graph:]&&[^\[\]<>()%\/]]+)/).nil? raise InvalidPDFInstructionError, "Operator: #{(stream.peek(10) + '...').inspect}" end operator = stream['operator'] PDF::Instruction.new(operator, *operands) else unless operands.empty? raise InvalidPDFInstructionError, "No operator given for operands: #{operands.map(&:to_s).join(' ')}" end end end end end end origami-2.0.0/lib/origami/graphics/patterns.rb0000644000004100000410000002054212757133666021375 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end module Origami module Graphics module Pattern module Type TILING = 1 SHADING = 2 end def self.included(receiver) receiver.field :Type, :Type => Name, :Default => :Pattern receiver.field :PatternType, :Type => Integer, :Required => true end class Tiling < ContentStream include Pattern include ResourcesHolder module PaintType COLOURED = 1 UNCOLOURED = 2 end module Type CONSTANT_SPACING = 1 NO_DISTORTION = 2 CONSTANT_SPACING_AND_FASTER_TILING = 3 end field :PatternType, :Type => Integer, :Default => Pattern::Type::TILING, :Required => true field :PaintType, :Type => Integer, :Required => true field :TilingType, :Type => Integer, :Required => true field :BBox, :Type => Array, :Required => true field :XStep, :Type => Number, :Required => true field :YStep, :Type => Number, :Required => true field :Resources, :Type => Resources, :Required => true field :Matrix, :Type => Array, :Default => [ 1, 0, 0, 1, 0, 0 ] end class Shading < Dictionary include StandardObject include Pattern module Type FUNCTIONBASED = 1 AXIAL = 2 RADIAL = 3 FREEFORM_TRIANGLE_MESH = 4 LATTICEFORM_TRIANGLE_MESH = 5 COONS_PATCH_MESH = 6 TENSORPRODUCT_PATCH_MESH = 7 end field :PatternType, :Type => Integer, :Default => Pattern::Type::SHADING, :Required => true field :Shading, :Type => [ Dictionary, Stream ], :Required => true field :Matrix, :Type => Array, :Default => [ 1, 0, 0, 1, 0, 0 ] field :ExtGState, :Type => Dictionary module ShadingObject def self.included(receiver) receiver.field :ShadingType, :Type => Integer, :Required => true receiver.field :ColorSpace, :Type => [ Name, Array ], :Required => true receiver.field :Background, :Type => Array receiver.field :BBox, :Type => Array receiver.field :AntiAlias, :Type => Boolean, :Default => false end end class FunctionBased < Dictionary include StandardObject include ShadingObject field :ShadingType, :Type => Integer, :Default => Shading::Type::FUNCTIONBASED, :Required => true field :Domain, :Type => Array, :Default => [ 0.0, 1.0, 0.0, 1.0 ] field :Matrix, :Type => Array, :Default => [ 1, 0, 0, 1, 0, 0 ] field :Function, :Type => [ Dictionary, Stream ], :Required => true end class Axial < Dictionary include StandardObject include ShadingObject field :ShadingType, :Type => Integer, :Default => Shading::Type::AXIAL, :Required => true field :Coords, :Type => Array, :Required => true field :Domain, :Type => Array, :Default => [ 0.0, 1.0 ] field :Function, :Type => [ Dictionary, Stream ], :Required => true field :Extend, :Type => Array, :Default => [ false, false ] end class Radial < Dictionary include StandardObject include ShadingObject field :ShadingType, :Type => Integer, :Default => Shading::Type::RADIAL, :Required => true field :Coords, :Type => Array, :Required => true field :Domain, :Type => Array, :Default => [ 0.0, 1.0 ] field :Function, :Type => [ Dictionary, Stream ], :Required => true field :Extend, :Type => Array, :Default => [ false, false ] end class FreeFormTriangleMesh < Stream include ShadingObject field :ShadingType, :Type => Integer, :Default => Shading::Type::FREEFORM_TRIANGLE_MESH, :Required => true field :BitsPerCoordinate, :Type => Integer, :Required => true field :BitsPerComponent, :Type => Integer, :Required => true field :BitsPerFlag, :Type => Integer, :Required => true field :Decode, :Type => Array, :Required => true field :Function, :Type => [ Dictionary, Stream ] end class LatticeFormTriangleMesh < Stream include ShadingObject field :ShadingType, :Type => Integer, :Default => Shading::Type::LATTICEFORM_TRIANGLE_MESH, :Required => true field :BitsPerCoordinate, :Type => Integer, :Required => true field :BitsPerComponent, :Type => Integer, :Required => true field :VerticesPerRow, :Type => Integer, :Required => true field :Decode, :Type => Array, :Required => true field :Function, :Type => [ Dictionary, Stream ] end class CoonsPathMesh < Stream include ShadingObject field :ShadingType, :Type => Integer, :Default => Shading::Type::COONS_PATCH_MESH, :Required => true field :BitsPerCoordinate, :Type => Integer, :Required => true field :BitsPerComponent, :Type => Integer, :Required => true field :BitsPerFlag, :Type => Integer, :Required => true field :Decode, :Type => Array, :Required => true field :Function, :Type => [ Dictionary, Stream ] end class TensorProductPatchMesh < Stream include ShadingObject field :ShadingType, :Type => Integer, :Default => Shading::Type::TENSORPRODUCT_PATCH_MESH, :Required => true field :BitsPerCoordinate, :Type => Integer, :Required => true field :BitsPerComponent, :Type => Integer, :Required => true field :BitsPerFlag, :Type => Integer, :Required => true field :Decode, :Type => Array, :Required => true field :Function, :Type => [ Dictionary, Stream ] end end end end class PDF::Instruction insn 'sh', Name end end origami-2.0.0/lib/origami/graphics/text.rb0000644000004100000410000001726712757133666020533 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end module Origami module Text OPERATORS = [ 'Tc', 'Tw', 'Tz', 'TL', 'Tf', 'Tr', 'Ts', # Text state 'BT', 'ET', # Text objects 'Td', 'TD', 'Tm', 'T*', # Positioning 'Tj', "'", '"', 'TJ' # Showing ] module Rendering FILL = 0 STROKE = 1 FILL_AND_STROKE = 2 INVISIBLE = 3 FILL_AND_CLIP = 4 STROKE_AND_CLIP = 5 FILL_AND_STROKE_AND_CLIP = 6 CLIP = 7 end class TextStateError < Error #:nodoc: end class State attr_accessor :char_spacing, :word_spacing, :scaling, :leading attr_accessor :font, :font_size attr_accessor :rendering_mode attr_accessor :text_rise, :text_knockout attr_accessor :text_matrix, :text_line_matrix, :text_rendering_matrix def initialize self.reset end def reset @char_spacing = 0 @word_spacing = 0 @scaling = 100 @leading = 0 @font = nil @font_size = nil @rendering_mode = Rendering::FILL @text_rise = 0 @text_knockout = true # # Text objects # @text_object = false @text_matrix = @text_line_matrix = @text_rendering_matrix = nil end def is_in_text_object? @text_object end def begin_text_object if is_in_text_object? raise TextStateError, "Cannot start a text object within an existing text object." end @text_object = true @text_matrix = @text_line_matrix = @text_rendering_matrix = Matrix.identity(3) end def end_text_object unless is_in_text_object? raise TextStateError, "Cannot end text object : no previous text object has begun." end @text_object = false @text_matrix = @text_line_matrix = @text_rendering_matrix = nil end end #class State end #module Text class PDF::Instruction # # Text instructions definitions # insn 'Tc', Real do |canvas, cS| canvas.gs.text_state.char_spacing = cS end insn 'Tw', Real do |canvas, wS| canvas.gs.text_state.word_spacing = wS end insn 'Tz', Real do |canvas, s| canvas.gs.text_state.scaling = s end insn 'TL', Real do |canvas, l| canvas.gs.text_state.leading = l end insn 'Tf', Name, Real do |canvas, font, size| canvas.gs.text_state.font = font canvas.gs.text_state.font_size = size end insn 'Tr', Integer do |canvas, r| canvas.gs.text_state.rendering_mode = r end insn 'Ts', Real do |canvas, s| canvas.gs.text_state.text_rise = s end insn 'BT' do |canvas| canvas.gs.text_state.begin_text_object end insn 'ET' do |canvas| canvas.gs.text_state.end_text_object end insn 'Td', Real, Real do |canvas, tx, ty| unless canvas.gs.text_state.is_in_text_object? raise TextStateError, "Must be in a text object to use operator : Td" end canvas.gs.text_state.text_matrix = canvas.gs.text_state.text_line_matrix = Matrix.rows([[1,0,0],[0,1,0],[tx, ty, 1]]) * canvas.gs.text_state.text_line_matrix end insn 'TD', Real, Real do |canvas, tx, ty| unless canvas.gs.text_state.is_in_text_object? raise TextStateError, "Must be in a text object to use operator : TD" end canvas.gs.text_state.leading = -ty canvas.gs.text_state.text_matrix = canvas.gs.text_state.text_line_matrix = Matrix.rows([[1,0,0],[0,1,0],[tx,ty,1]]) * canvas.gs.text_state.text_line_matrix end insn 'Tm', Real, Real, Real, Real, Real, Real do |canvas, a,b,c,d,e,f,g| unless canvas.gs.text_state.is_in_text_object? raise TextStateError, "Must be in a text object to use operator : Tm" end canvas.gs.text_state.text_matrix = canvas.gs.text_state.text_line_matrix = Matrix.rows([[a,b,0],[c,d,0],[e,f,1]]) end insn 'T*' do |canvas| unless canvas.gs.text_state.is_in_text_object? raise TextStateError, "Must be in a text object to use operator : T*" end tx, ty = 0, -canvas.gs.text_state.leading canvas.gs.text_state.text_matrix = canvas.gs.text_state.text_line_matrix = Matrix.rows([[1,0,0],[0,1,0],[tx, ty, 1]]) * canvas.gs.text_state.text_line_matrix end insn 'Tj', String do |canvas, s| unless canvas.gs.text_state.is_in_text_object? raise TextStateError, "Must be in a text object to use operator : Tj" end canvas.write_text(s) end insn "'", String do |canvas, s| unless canvas.gs.text_state.is_in_text_object? raise TextStateError, "Must be in a text object to use operator : '" end tx, ty = 0, -canvas.gs.text_state.leading canvas.gs.text_state.text_matrix = canvas.gs.text_state.text_line_matrix = Matrix.rows([[1,0,0],[0,1,0],[tx, ty, 1]]) * canvas.gs.text_state.text_line_matrix canvas.write_text(s) end insn '"', Real, Real, String do |canvas, w, c, s| unless canvas.gs.text_state.is_in_text_object? raise TextStateError, "Must be in a text object to use operator : \"" end canvas.gs.text_state.word_spacing = w canvas.gs.text_state.char_spacing = c tx, ty = 0, -gs.text_state.leading canvas.gs.text_state.text_matrix = canvas.gs.text_state.text_line_matrix = Matrix.rows([[1,0,0],[0,1,0],[tx, ty, 1]]) * canvas.gs.text_state.text_line_matrix canvas.write_text(s) end insn 'TJ', Array do |canvas, arr| arr.each do |g| case g when Fixnum,Float then # XXX: handle this in text space ? when ::String then canvas.write_text(g) else raise InvalidPDFInstructionError, "Invalid component type `#{g.class}` in TJ operand" end end end end end origami-2.0.0/lib/origami/graphics/path.rb0000644000004100000410000001130312757133666020464 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end module Origami module Graphics module LineCapStyle BUTT_CAP = 0 ROUND_CAP = 1 PROJECTING_SQUARE_CAP = 2 end module LineJoinStyle MITER_JOIN = 0 ROUND_JOIN = 1 BEVEL_JOIN = 2 end class DashPattern attr_accessor :array, :phase def initialize(array, phase = 0) @array = array @phase = phase end def eql?(dash) #:nodoc dash.array == @array and dash.phase == @phase end def hash #:nodoc: [ @array, @phase ].hash end end class InvalidPathError < Error #:nodoc: end class Path module Segment attr_accessor :from, :to def initialize(from, to) @from, @to = from, to end end class Line include Segment end attr_accessor :current_point attr_reader :segments def initialize @segments = [] @current_point = nil @closed = false end def is_closed? @closed end def close! from = @current_point to = @segments.first.from @segments << Line.new(from, to) @segments.freeze @closed = true end def add_segment(seg) raise GraphicsStateError, "Cannot modify closed subpath" if is_closed? @segments << seg @current_point = seg.to end end end class PDF::Instruction insn 'm', Real, Real do |canvas, x,y| canvas.gs.current_path << (subpath = Graphics::Path.new) subpath.current_point = [x,y] end insn 'l', Real, Real do |canvas, x,y| if canvas.gs.current_path.empty? raise InvalidPathError, "No current point is defined" end subpath = canvas.gs.current_path.last from = subpath.current_point to = [x,y] subpath.add_segment(Graphics::Path::Line.new(from, to)) end insn 'h' do |canvas| unless canvas.gs.current_path.empty? subpath = canvas.gs.current_path.last subpath.close! unless subpath.is_closed? end end insn 're', Real, Real, Real, Real do |canvas, x,y,width,height| tx = x + width ty = y + height canvas.gs.current_path << (subpath = Graphics::Path.new) subpath.segments << Graphics::Path::Line.new([x,y], [tx,y]) subpath.segments << Graphics::Path::Line.new([tx,y], [tx, ty]) subpath.segments << Graphics::Path::Line.new([tx, ty], [x, ty]) subpath.close! end insn 'S' do |canvas| canvas.stroke_path end insn 's' do |canvas| canvas.gs.current_path.last.close! canvas.stroke_path end insn 'f' do |canvas| canvas.fill_path end insn 'F' do |canvas| canvas.fill_path end insn 'f*' do |canvas| canvas.fill_path end insn 'B' do |canvas| canvas.fill_path canvas.stroke_path end insn 'B*' do |canvas| canvas.fill_path canvas.stroke_path end insn 'b' do |canvas| canvas.gs.current_path.last.close! canvas.fill_path canvas.stroke_path end insn 'b*' do |canvas| canvas.gs.current_path.last.close! canvas.fill_path canvas.stroke_path end insn 'n' end end origami-2.0.0/lib/origami/graphics/render.rb0000644000004100000410000000302512757133666021011 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end module Origami module Graphics module Canvas attr_reader :gs def initialize @gs = Graphics::State.new end def clear @gs.reset end def write_text(s); end def stroke_path; end def fill_path; end end class DummyCanvas include Canvas end class TextCanvas include Canvas def initialize(output = STDOUT, columns = 80, lines = 25) super() @output = output @columns, @lines = columns, lines end def write_text(s) @output.print(s) end end end end origami-2.0.0/lib/origami/graphics/colors.rb0000644000004100000410000001576012757133666021044 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end module Origami module Graphics class InvalidColorError < Error #:nodoc: end module Color module Intent ABSOLUTE = :AbsoluteColorimetric RELATIVE = :RelativeColorimetric SATURATION = :Saturation PERCEPTUAL = :Perceptual end module BlendMode NORMAL = :Normal COMPATIBLE = :Compatible MULTIPLY = :Multiply SCREEN = :Screen OVERLAY = :Overlay DARKEN = :Darken LIGHTEN = :Lighten COLORDODGE = :ColorDodge COLORBURN = :ColorBurn HARDLIGHT = :HardLight SOFTLIGHt = :SoftLight DIFFERENCE = :Difference EXCLUSION = :Exclusion end module Space DEVICE_GRAY = :DeviceGray DEVICE_RGB = :DeviceRGB DEVICE_CMYK = :DeviceCMYK end def self.cmyk_to_rgb(c, m, y, k) r = 1 - (( c * ( 1 - k ) + k )) g = 1 - (( m * ( 1 - k ) + k )) b = 1 - (( y * ( 1 - k ) + k )) [ r, g, b ] end def self.gray_to_rgb(g) [ g, g, g ] end # # Class representing an embedded ICC Profile stream. # class ICCProfile < Stream field :N, :Type => Integer, :Required => true, :Version => '1.3' field :Alternate, :Type => [ Name, Array ] field :Range, :Type => Array field :Metadata, :Type => Stream, :Version => '1.4' end class GrayScale attr_accessor :g def initialize(g) @g = g end end class RGB attr_accessor :r,:g,:b def initialize(r,g,b) @r,@g,@b = r,g,b end end class CMYK attr_accessor :c,:m,:y,:k def initialize(c,m,y,k) @c,@m,@y,@k = c,m,y,k end end def Color.to_a(color) return color if color.is_a?(::Array) if (color.respond_to? :r and color.respond_to? :g and color.respond_to? :b) r = (color.respond_to?(:r) ? color.r : color[0]).to_f / 255 g = (color.respond_to?(:g) ? color.g : color[1]).to_f / 255 b = (color.respond_to?(:b) ? color.b : color[2]).to_f / 255 return [r, g, b] elsif (color.respond_to? :c and color.respond_to? :m and color.respond_to? :y and color.respond_to? :k) c = (color.respond_to?(:c) ? color.c : color[0]).to_f m = (color.respond_to?(:m) ? color.m : color[1]).to_f y = (color.respond_to?(:y) ? color.y : color[2]).to_f k = (color.respond_to?(:k) ? color.k : color[3]).to_f return [c,m,y,k] elsif color.respond_to?:g or (0.0..1.0) === color g = color.respond_to?(:g) ? color.g : color return [ g ] else raise TypeError, "Invalid color : #{color}" end end end end class PDF::Instruction insn 'CS', Name do |canvas, cs| canvas.gs.stroking_colorspace = cs end insn 'cs', Name do |canvas, cs| canvas.gs.nonstroking_colorspace = cs end insn 'SC', '*' do |canvas, *c| canvas.gs.stroking_color = c end insn 'sc', '*' do |canvas, *c| canvas.gs.nonstroking_color = c end insn 'G', Real do |canvas, c| unless (0..1).include? c raise Graphics::InvalidColorError, "Not a valid color for DeviceGray: #{c}" end canvas.gs.stroking_colorspace = Graphics::Color::Space::DEVICE_GRAY canvas.gs.stroking_color = [ c ] end insn 'g', Real do |canvas, c| unless (0..1).include? c raise Graphics::InvalidColorError, "Not a valid color for DeviceGray: #{c}" end canvas.gs.nonstroking_colorspace = Graphics::Color::Space::DEVICE_GRAY canvas.gs.nonstroking_color = [ c ] end insn 'RG', Real, Real, Real do |canvas, r,g,b| color = [ r, g, b ] unless color.all? {|comp| (0..1).include? comp} raise Graphics::InvalidColorError, "Not a valid color for DeviceRGB: #{color.inspect}" end canvas.gs.stroking_colorspace = Graphics::Color::Space::DEVICE_RGB canvas.gs.stroking_color = color end insn 'rg', Real, Real, Real do |canvas, r,g,b| color = [ r, g, b ] unless color.all? {|comp| (0..1).include? comp} raise Graphics::InvalidColorError, "Not a valid color for DeviceRGB: #{color.inspect}" end canvas.gs.nonstroking_colorspace = Graphics::Color::Space::DEVICE_RGB canvas.gs.nonstroking_color = color end insn 'K', Real, Real, Real, Real do |canvas, c,m,y,k| color = [ c, m, y, k ] unless color.all? {|comp| (0..1).include? comp} raise Graphics::InvalidColorError, "Not a valid color for DeviceCMYK: #{color.inspect}" end canvas.gs.stroking_colorspace = Graphics::Color::Space::DEVICE_CMYK canvas.gs.stroking_color = color end insn 'k', Real, Real, Real, Real do |canvas, c,m,y,k| color = [ c, m, y, k ] unless color.all? {|comp| (0..1).include? comp} raise Graphics::InvalidColorError, "Not a valid color for DeviceCMYK: #{color.inspect}" end canvas.gs.nonstroking_colorspace = Graphics::Color::Space::DEVICE_CMYK canvas.gs.nonstroking_color = color end end end origami-2.0.0/lib/origami/graphics/xobject.rb0000644000004100000410000010037012757133666021171 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end module Origami # # A class representing a Stream containing the contents of a Page. # class ContentStream < Stream DEFAULT_SIZE = 12 DEFAULT_FONT = :F1 DEFAULT_LEADING = 20 DEFAULT_STROKE_COLOR = Graphics::Color::GrayScale.new(0.0) DEFAULT_FILL_COLOR = Graphics::Color::GrayScale.new(1.0) DEFAULT_LINECAP = Graphics::LineCapStyle::BUTT_CAP DEFAULT_LINEJOIN = Graphics::LineJoinStyle::MITER_JOIN DEFAULT_DASHPATTERN = Graphics::DashPattern.new([], 0) DEFAULT_LINEWIDTH = 1.0 attr_accessor :canvas def initialize(data = "", dictionary = {}) super @instructions = nil @canvas = Graphics::DummyCanvas.new end def render(engine) load! if @instructions.nil? @instructions.each do |instruction| instruction.render(engine) end nil end def pre_build #:nodoc: unless @instructions.nil? if @canvas.gs.text_state.is_in_text_object? @instructions << PDF::Instruction.new('ET').render(@canvas) end @data = @instructions.join end super end def instructions load! if @instructions.nil? @instructions end def draw_image(name, attr = {}) load! if @instructions.nil? x, y = attr[:x], attr[:y] @instructions << PDF::Instruction.new('q') @instructions << PDF::Instruction.new('cm', 300, 0, 0, 300, x, y) @instructions << PDF::Instruction.new('Do', name) @instructions << PDF::Instruction.new('Q') end # # Draw a straight line from the point at coord _from_, to the point at coord _to_. # def draw_line(from, to, attr = {}) draw_polygon([from, to], attr) end # # Draw a polygon from a array of coordinates. # def draw_polygon(coords = [], attr = {}) load! if @instructions.nil? stroke_color = attr[:stroke_color] || DEFAULT_STROKE_COLOR fill_color = attr[:fill_color] || DEFAULT_FILL_COLOR line_cap = attr[:line_cap] || DEFAULT_LINECAP line_join = attr[:line_join] || DEFAULT_LINEJOIN line_width = attr[:line_width] || DEFAULT_LINEWIDTH dash_pattern = attr[:dash] || DEFAULT_DASHPATTERN stroke = attr[:stroke].nil? ? true : attr[:stroke] fill = attr[:fill].nil? ? false : attr[:fill] stroke = true if fill == false and stroke == false set_fill_color(fill_color) if fill set_stroke_color(stroke_color) if stroke set_line_width(line_width) set_line_cap(line_cap) set_line_join(line_join) set_dash_pattern(dash_pattern) if @canvas.gs.text_state.is_in_text_object? @instructions << PDF::Instruction.new('ET').render(@canvas) end unless coords.size < 1 x,y = coords.slice!(0) @instructions << PDF::Instruction.new('m',x,y).render(@canvas) coords.each do |px,py| @instructions << PDF::Instruction.new('l',px,py).render(@canvas) end @instructions << (i = if stroke and not fill PDF::Instruction.new('s') elsif fill and not stroke PDF::Instruction.new('f') elsif fill and stroke PDF::Instruction.new('b') end ) i.render(@canvas) end self end # # Draw a rectangle at position (_x_,_y_) with defined _width_ and _height_. # def draw_rectangle(x, y, width, height, attr = {}) load! if @instructions.nil? stroke_color = attr[:stroke_color] || DEFAULT_STROKE_COLOR fill_color = attr[:fill_color] || DEFAULT_FILL_COLOR line_cap = attr[:line_cap] || DEFAULT_LINECAP line_join = attr[:line_join] || DEFAULT_LINEJOIN line_width = attr[:line_width] || DEFAULT_LINEWIDTH dash_pattern = attr[:dash] || DEFAULT_DASHPATTERN stroke = attr[:stroke].nil? ? true : attr[:stroke] fill = attr[:fill].nil? ? false : attr[:fill] stroke = true if fill == false and stroke == false set_fill_color(fill_color) if fill set_stroke_color(stroke_color) if stroke set_line_width(line_width) set_line_cap(line_cap) set_line_join(line_join) set_dash_pattern(dash_pattern) if @canvas.gs.text_state.is_in_text_object? @instructions << PDF::Instruction.new('ET').render(@canvas) end @instructions << PDF::Instruction.new('re', x,y,width,height).render(@canvas) @instructions << (i = if stroke and not fill PDF::Instruction.new('S') elsif fill and not stroke PDF::Instruction.new('f') elsif fill and stroke PDF::Instruction.new('B') end ) i.render(@canvas) self end # # Adds text to the content stream with custom formatting attributes. # _text_:: Text to write. # _attr_:: Formatting attributes. # def write(text, attr = {}) load! if @instructions.nil? x,y = attr[:x], attr[:y] font = attr[:font] || DEFAULT_FONT size = attr[:size] || DEFAULT_SIZE leading = attr[:leading] || DEFAULT_LEADING color = attr[:color] || attr[:fill_color] || DEFAULT_STROKE_COLOR stroke_color = attr[:stroke_color] || DEFAULT_STROKE_COLOR line_width = attr[:line_width] || DEFAULT_LINEWIDTH word_spacing = attr[:word_spacing] char_spacing = attr[:char_spacing] scale = attr[:scale] rise = attr[:rise] rendering = attr[:rendering] @instructions << PDF::Instruction.new('ET').render(@canvas) if (x or y) and @canvas.gs.text_state.is_in_text_object? unless @canvas.gs.text_state.is_in_text_object? @instructions << PDF::Instruction.new('BT').render(@canvas) end set_text_font(font, size) set_text_pos(x, y) if x or y set_text_leading(leading) if leading set_text_rendering(rendering) if rendering set_text_rise(rise) if rise set_text_scale(scale) if scale set_text_word_spacing(word_spacing) if word_spacing set_text_char_spacing(char_spacing) if char_spacing set_fill_color(color) set_stroke_color(stroke_color) set_line_width(line_width) write_text_block(text) self end def paint_shading(shade) load! if @instructions.nil? @instructions << PDF::Instruction.new('sh', shade).render(@canvas) self end def set_text_font(fontname, size) load! if @instructions.nil? if fontname != @canvas.gs.text_state.font or size != @canvas.gs.text_state.font_size @instructions << PDF::Instruction.new('Tf', fontname, size).render(@canvas) end self end def set_text_pos(tx,ty) load! if @instructions.nil? @instructions << PDF::Instruction.new('Td', tx, ty).render(@canvas) self end def set_text_leading(leading) load! if @instructions.nil? if leading != @canvas.gs.text_state.leading @instructions << PDF::Instruction.new('TL', leading).render(@canvas) end self end def set_text_rendering(rendering) load! if @instructions.nil? if rendering != @canvas.gs.text_state.rendering_mode @instructions << PDF::Instruction.new('Tr', rendering).render(@canvas) end self end def set_text_rise(rise) load! if @instructions.nil? if rise != @canvas.gs.text_state.text_rise @instructions << PDF::Instruction.new('Ts', rise).render(@canvas) end self end def set_text_scale(scaling) load! if @instructions.nil? if scale != @canvas.gs.text_state.scaling @instructions << PDF::Instruction.new('Tz', scaling).render(@canvas) end self end def set_text_word_spacing(word_spacing) load! if @instructions.nil? if word_spacing != @canvas.gs.text_state.word_spacing @instructions << PDF::Instruction.new('Tw', word_spacing).render(@canvas) end self end def set_text_char_spacing(char_spacing) load! if @instructions.nil? if char_spacing != @canvas.gs.text_state.char_spacing @instructions << PDF::Instruction.new('Tc', char_spacing).render(@canvas) end self end def set_fill_color(color) load! if @instructions.nil? @instructions << ( i = if (color.respond_to? :r and color.respond_to? :g and color.respond_to? :b) or (color.is_a?(::Array) and color.size == 3) r = (color.respond_to?(:r) ? color.r : color[0]).to_f / 255 g = (color.respond_to?(:g) ? color.g : color[1]).to_f / 255 b = (color.respond_to?(:b) ? color.b : color[2]).to_f / 255 PDF::Instruction.new('rg', r, g, b) if @canvas.gs.nonstroking_color != [r,g,b] elsif (color.respond_to? :c and color.respond_to? :m and color.respond_to? :y and color.respond_to? :k) or (color.is_a?(::Array) and color.size == 4) c = (color.respond_to?(:c) ? color.c : color[0]).to_f m = (color.respond_to?(:m) ? color.m : color[1]).to_f y = (color.respond_to?(:y) ? color.y : color[2]).to_f k = (color.respond_to?(:k) ? color.k : color[3]).to_f PDF::Instruction.new('k', c, m, y, k) if @canvas.gs.nonstroking_color != [c,m,y,k] elsif color.respond_to?:g or (0.0..1.0) === color g = color.respond_to?(:g) ? color.g : color PDF::Instruction.new('g', g) if @canvas.gs.nonstroking_color != [ g ] else raise TypeError, "Invalid color : #{color}" end ) i.render(@canvas) if i self end def set_stroke_color(color) load! if @instructions.nil? @instructions << ( i = if (color.respond_to? :r and color.respond_to? :g and color.respond_to? :b) or (color.is_a?(::Array) and color.size == 3) r = (color.respond_to?(:r) ? color.r : color[0]).to_f / 255 g = (color.respond_to?(:g) ? color.g : color[1]).to_f / 255 b = (color.respond_to?(:b) ? color.b : color[2]).to_f / 255 PDF::Instruction.new('RG', r, g, b) if @canvas.gs.stroking_color != [r,g,b] elsif (color.respond_to? :c and color.respond_to? :m and color.respond_to? :y and color.respond_to? :k) or (color.is_a?(::Array) and color.size == 4) c = (color.respond_to?(:c) ? color.c : color[0]).to_f m = (color.respond_to?(:m) ? color.m : color[1]).to_f y = (color.respond_to?(:y) ? color.y : color[2]).to_f k = (color.respond_to?(:k) ? color.k : color[3]).to_f PDF::Instruction.new('K', c, m, y, k) if @canvas.gs.stroking_color != [c,m,y,k] elsif color.respond_to?:g or (0.0..1.0) === color g = color.respond_to?(:g) ? color.g : color PDF::Instruction.new('G', g) if @canvas.gs.stroking_color != [ g ] else raise TypeError, "Invalid color : #{color}" end ) i.render(@canvas) if i self end def set_dash_pattern(pattern) load! if @instructions.nil? unless @canvas.gs.dash_pattern.eql? pattern @instructions << PDF::Instruction.new('d', pattern.array, pattern.phase).render(@canvas) end self end def set_line_width(width) load! if @instructions.nil? if @canvas.gs.line_width != width @instructions << PDF::Instruction.new('w', width).render(@canvas) end self end def set_line_cap(cap) load! if @instructions.nil? if @canvas.gs.line_cap != cap @instructions << PDF::Instruction.new('J', cap).render(@canvas) end self end def set_line_join(join) load! if @instructions.nil? if @canvas.gs.line_join != join @instructions << PDF::Instruction.new('j', join).render(@canvas) end self end private def load! decode! code = StringScanner.new self.data @instructions = [] until code.eos? insn = PDF::Instruction.parse(code) @instructions << insn if insn end self end def write_text_block(text) lines = text.split("\n").map!{|line| line.to_s} @instructions << PDF::Instruction.new('Tj', lines.slice!(0)).render(@canvas) lines.each do |line| @instructions << PDF::Instruction.new("'", line).render(@canvas) end end end #class ContentStream class Page < Dictionary def render(engine) #:nodoc: contents = self.Contents contents = [ contents ] unless contents.is_a? Array contents.each do |stream| stream = stream.cast_to(ContentStream) unless stream.is_a? ContentStream stream.render(engine) end end # TODO :nodoc: def draw_image raise NotImplementedError end # See ContentStream#draw_line. def draw_line(from, to, attr = {}) last_content_stream.draw_line(from, to, attr); self end # See ContentStream#draw_polygon. def draw_polygon(coords = [], attr = {}) last_content_stream.draw_polygon(coords, attr); self end # See ContentStream#draw_rectangle. def draw_rectangle(x, y, width, height, attr = {}) last_content_stream.draw_rectangle(x, y, width, height, attr); self end # See ContentStream#write. def write(text, attr = {}) last_content_stream.write(text, attr); self end # TODO :nodoc: def paint_shading(shade) raise NotImplementedError end # TODO :nodoc: def set_text_font(font, size) raise NotImplementedError end # See ContentStream#set_text_pos. def set_text_pos(tx, ty) last_content_stream.set_text_pos(tx, ty); self end # See ContentStream#set_text_leading. def set_text_leading(leading) last_content_stream.set_text_leading(leading); self end # See ContentStream#set_text_rendering. def set_text_rendering(rendering) last_content_stream.set_text_rendering(rendering); self end # See ContentStream#set_text_rise. def set_text_rise(rise) last_content_stream.set_text_rise(rise); self end # See ContentStream#set_text_scale. def set_text_scale(scaling) last_content_stream.set_text_scale(scaling); self end # See ContentStream#set_text_word_spacing. def set_text_word_spacing(word_spacing) last_content_stream.set_text_word_spacing(word_spacing); self end # See ContentStream#set_text_char_spacing. def set_text_char_spacing(char_spacing) last_content_stream.set_text_char_spacing(char_spacing); self end # See ContentStream#set_fill_color. def set_fill_color(color) last_content_stream.set_fill_color(color); self end # See ContentStream#set_stroke_color. def set_stroke_color(color) last_content_stream.set_stroke_color(color); self end # See ContentStream#set_dash_pattern. def set_dash_pattern(pattern) last_content_stream.set_dash_pattern(pattern); self end # See ContentStream#set_line_width. def set_line_width(width) last_content_stream.set_line_width(width); self end # See ContentStream#set_line_cap. def set_line_cap(cap) last_content_stream.set_line_cap(cap); self end # See ContentStream#set_line_join. def set_line_join(join) last_content_stream.set_line_join(join); self end private def last_content_stream #:nodoc: streams = self.content_streams if streams.empty? self.Contents = ContentStream.new else streams.last end end end # class Page module Graphics module XObject def self.included(receiver) receiver.field :Type, :Type => Name, :Default => :XObject end end class FormXObject < ContentStream include XObject include ResourcesHolder class Group < Dictionary include StandardObject module Type TRANSPARENCY = :Transparency end field :Type, :Type => Name, :Default => :Group field :S, :Type => Name, :Default => Type::TRANSPARENCY, :Required => true end class Reference < Dictionary include StandardObject field :F, :Type => FileSpec, :Required => true field :Page, :Type => [ Integer, String ], :Required => true field :ID, :Type => Array.of(String, length: 2) end field :Subtype, :Type => Name, :Default => :Form, :Required => true field :FormType, :Type => Integer, :Default => 1 field :BBox, :Type => Rectangle, :Required => true field :Matrix, :Type => Array.of(Number, length: 6), :Default => [1, 0, 0, 1, 0, 0] field :Resources, :Type => Resources, :Version => "1.2" field :Group, :Type => Group, :Version => "1.4" field :Ref, :Type => Reference, :Version => "1.4" field :Metadata, :Type => MetadataStream, :Version => "1.4" field :PieceInfo, :Type => Dictionary, :Version => "1.3" field :LastModified, :Type => String, :Version => "1.3" field :StructParent, :Type => Integer, :Version => "1.3" field :StructParents, :Type => Integer, :Version => "1.3" field :OPI, :Type => Dictionary, :Version => "1.2" field :OC, :Type => Dictionary, :Version => "1.5" field :Name, :Type => Name field :Measure, :Type => Dictionary, :Version => "1.7", :ExtensionLevel => 3 field :PtData, :Type => Dictionary, :Version => "1.7", :ExtensionLevel => 3 def pre_build self.Resources = Resources.new.pre_build unless has_field?(:Resources) super end end class ImageXObject < Stream include XObject field :Subtype, :Type => Name, :Default => :Image, :Required => true field :Width, :Type => Integer, :Required => true field :Height, :Type => Integer, :Required => true field :ColorSpace, :Type => [ Name, Array ] field :BitsPerComponent, :Type => Integer field :Intent, :Type => Name, :Version => "1.1" field :ImageMask, :Type => Boolean, :Default => false field :Mask, :Type => [ ImageXObject, Array.of(Integer) ], :Version => "1.3" field :Decode, :Type => Array.of(Number) field :Interpolate, :Type => Boolean, :Default => false field :Alternates, :Type => Array, :Version => "1.3" field :SMask, :Type => ImageXObject, :Version => "1.4" field :SMaskInData, :Type => Integer, :Default => 0, :Version => "1.5" field :Name, :Type => Name field :StructParent, :Type => Integer, :Version => "1.3" field :ID, :Type => String, :Version => "1.3" field :OPI, :Type => Dictionary, :Version => "1.2" field :Matte, :Type => Array.of(Number), :Version => "1.4" # Used in Soft-Mask images. field :Metadata, :Type => MetadataStream, :Version => "1.4" field :OC, :Type => Dictionary, :Version => "1.5" field :Measure, :Type => Dictionary, :Version => "1.7", :ExtensionLevel => 3 field :PtData, :Type => Dictionary, :Version => "1.7", :ExtensionLevel => 3 def self.from_image_file(path, format = nil) if path.respond_to?(:read) data = fd.read else data = File.binread(File.expand_path(path)) format ||= File.extname(path) format.slice!(0) if format and format[0,1] == '.' end image = ImageXObject.new raise ArgumentError, "Missing file format" if format.nil? case format.downcase when 'jpg', 'jpeg', 'jpe', 'jif', 'jfif', 'jfi' image.setFilter :DCTDecode image.encoded_data = data image when 'jp2','jpx','j2k','jpf','jpm','mj2' image.setFilter :JPXDecode image.encoded_data = data image when 'jb2', 'jbig', 'jbig2' image.setFilter :JBIG2Decode image.encoded_data = data image else raise NotImplementedError, "Unknown file format: '#{format}'" end end # # Converts an ImageXObject stream into an image file data. # Output format depends on the stream encoding: # * JPEG for DCTDecode # * JPEG2000 for JPXDecode # * JBIG2 for JBIG2Decode # * PNG for everything else # # Returns an array of the form [ _format_, _data_ ] # def to_image_file encoding = self.Filter encoding = encoding[0] if encoding.is_a? ::Array case (encoding && encoding.value) when :DCTDecode then return [ 'jpg', self.data ] when :JBIG2Decode then return [ 'jbig2', self.data ] when :JPXDecode then return [ 'jp2', self.data ] end # Assume PNG data. raise InvalidColorError, "No colorspace specified" unless self.ColorSpace case cs = self.ColorSpace.value when Color::Space::DEVICE_GRAY colortype = 0 components = 1 when Color::Space::DEVICE_RGB colortype = 2 components = 3 when ::Array cstype = cs[0].is_a?(Reference) ? cs[0].solve : cs[0] case cstype.value when :Indexed colortype = 3 components = 3 csbase = cs[1].is_a?(Reference) ? cs[1].solve : cs[1] lookup = cs[3].is_a?(Reference) ? cs[3].solve : cs[3] when :ICCBased iccprofile = cs[1].is_a?(Reference) ? cs[1].solve : cs[1] raise InvalidColorError, "Invalid ICC Profile parameter" unless iccprofile.is_a?(Stream) case iccprofile.N when 1 colortype = 0 components = 1 when 3 colortype = 2 components = 3 else raise InvalidColorError, "Invalid number of components in ICC profile: #{iccprofile.N}" end else raise InvalidColorError, "Unsupported color space: #{self.ColorSpace}" end else raise InvalidColorError, "Unsupported color space: #{self.ColorSpace}" end bpc = self.BitsPerComponent || 8 w,h = self.Width, self.Height pixels = self.data hdr = [137, 80, 78, 71, 13, 10, 26, 10].pack('C*') chunks = [] chunks << [ 'IHDR', [ w, h, bpc, colortype, 0, 0, 0 ].pack("N2C5") ] if self.Intents intents = case self.Intents.value when Intents::PERCEPTUAL then 0 when Intents::RELATIVE then 1 when Intents::SATURATION then 2 when Intents::ABSOLUTE then 3 else 3 end chunks << [ 'sRGB', [ intents ].pack('C') ] chunks << [ 'gAMA', [ 45455 ].pack("N") ] chunks << [ 'cHRM', [ 31270, 32900, 64000, 33000, 30000, 60000, 15000, 6000 ].pack("N8") ] end if colortype == 3 lookup = case lookup when Stream then lookup.data when String then lookup.value else raise InvalidColorError, "Invalid indexed palette table" end raise InvalidColorError, "Invalid base color space" unless csbase palette = "" case csbase.value when Color::Space::DEVICE_GRAY lookup.each_byte do |g| palette << Color.gray_to_rgb(g).pack("C3") end when Color::Space::DEVICE_RGB palette << lookup[0, (lookup.size / 3) * 3] when Color::Space::DEVICE_CMYK (lookup.size / 4).times do |i| cmyk = lookup[i * 4, 4].unpack("C4").map!{|c| c.to_f / 255} palette << Color.cmyk_to_rgb(*cmyk).map!{|c| (c * 255).to_i}.pack("C3") end when ::Array case csbase[0].solve.value when :ICCBased iccprofile = csbase[1].solve raise InvalidColorError, "Invalid ICC Profile parameter" unless iccprofile.is_a?(Stream) case iccprofile.N when 1 lookup.each_byte do |g| palette << Color.gray_to_rgb(g).pack("C3") end when 3 palette << lookup[0, (lookup.size / 3) * 3] else raise InvalidColorError, "Invalid number of components in ICC profile: #{iccprofile.N}" end else raise InvalidColorError, "Unsupported color space: #{csbase}" end else raise InvalidColorError, "Unsupported color space: #{csbase}" end if iccprofile chunks << [ 'iCCP', 'ICC Profile' + "\x00\x00" + Zlib::Deflate.deflate(iccprofile.data, Zlib::BEST_COMPRESSION) ] end chunks << [ 'PLTE', palette ] bpr = w else # colortype != 3 if iccprofile chunks << [ 'iCCP', 'ICC Profile' + "\x00\x00" + Zlib::Deflate.deflate(iccprofile.data, Zlib::BEST_COMPRESSION) ] end bpr = (bpc >> 3) * components * w end nrows = pixels.size / bpr nrows.times do |irow| pixels.insert(irow * bpr + irow, "\x00") end chunks << [ 'IDAT', Zlib::Deflate.deflate(pixels, Zlib::BEST_COMPRESSION) ] if self.Metadata.is_a?(Stream) chunks << [ 'tEXt', "XML:com.adobe.xmp" + "\x00" + self.Metadata.data ] end chunks << [ 'IEND', '' ] [ 'png', hdr + chunks.map!{ |chk| [ chk[1].size, chk[0], chk[1], Zlib.crc32(chk[0] + chk[1]) ].pack("NA4A*N") }.join ] end end class ReferenceDictionary < Dictionary include StandardObject field :F, :Type => Dictionary, :Required => true field :Page, :Type => [Integer, String], :Required => true field :ID, :Tyoe => Array end end end origami-2.0.0/lib/origami/graphics/state.rb0000644000004100000410000001437012757133666020657 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end require 'matrix' module Origami module Graphics class GraphicsStateError < Error #:nodoc: end class State # # Device-independent parameters. # attr_accessor :ctm attr_accessor :clipping_path attr_accessor :stroking_colorspace, :nonstroking_colorspace, :stroking_color, :nonstroking_color attr_accessor :text_state attr_accessor :line_width, :line_cap, :line_join, :miter_limit, :dash_pattern attr_accessor :rendering_intent attr_accessor :stroke_adjustment attr_accessor :blend_mode, :soft_mask, :alpha_constant, :alpha_source attr_reader :current_path def initialize @stack = [] @current_path = [] @text_state = Text::State.new self.reset end def reset @ctm = Matrix.identity(3) @clipping_path = nil @stroking_colorspace = @nonstroking_colorspace = Color::Space::DEVICE_GRAY @stroking_color = @nonstroking_color = [ 0.0 ] #black @text_state.reset @line_width = 1.0 @line_cap = LineCapStyle::BUTT_CAP @line_join = LineJoinStyle::MITER_JOIN @miter_limit = 10.0 @dash_pattern = DashPattern.new([], 0) @rendering_intent = Color::Intent::RELATIVE @stroke_adjustment = false @blend_mode = Color::BlendMode::NORMAL @soft_mask = :None @alpha_constant = 1.0 @alpha_source = false end def save context = [ @ctm, @clipping_path, @stroking_colorspace, @nonstroking_colorspace, @stroking_color, @nonstroking_color, @text_state, @line_width, @line_cap, @line_join, @miter_limit, @dash_pattern, @rendering_intent, @stroke_adjustment, @blend_mode, @soft_mask, @alpha_constant, @alpha_source ] @stack.push(context) end def restore raise GraphicsStateError, "Cannot restore context : empty stack" if @stack.empty? @ctm, @clipping_path, @stroking_colorspace, @nonstroking_colorspace, @stroking_color, @nonstroking_color, @text_state, @line_width, @line_cap, @line_join, @miter_limit, @dash_pattern, @rendering_intent, @stroke_adjustment, @blend_mode, @soft_mask, @alpha_constant, @alpha_source = @stack.pop end end # # Generic Graphic state # 4.3.4 Graphics State Parameter Dictionaries p219 # class ExtGState < Dictionary include StandardObject field :Type, :Type => Name, :Default => :ExtGState, :Required => true field :LW, :Type => Integer, :Version => "1.3" field :LC, :Type => Integer, :Version => "1.3" field :LJ, :Type => Integer, :Version => "1.3" field :ML, :Type => Number, :Version => "1.3" field :D, :Type => Array.of(Array, Integer, length: 2), :Version => "1.3" field :RI, :Type => Name, :Version => "1.3" field :OP, :Type => Boolean field :op, :Type => Boolean, :Version => "1.3" field :OPM, :Type => Number, :Version => "1.3" field :Font, :Type => Array, :Version => "1.3" field :BG, :Type => Object field :BG2, :Type => Object, :Version => "1.3" field :UCR, :Type => Object field :UCR2, :Type => Object, :Version => "1.3" field :TR, :Type => Object field :TR2, :Type => Object, :Version => "1.3" field :HT, :Type => [ Dictionary, Name, Stream ] field :FL, :Type => Number, :Version => "1.3" field :SM, :Type => Number, :Version => "1.3" field :SA, :Type => Boolean field :BM, :Type => [ Name, Array ], :Version => "1.4" field :SMask, :Type => [ Dictionary, Name ], :Version => "1.4" field :CA, :Type => Number field :ca, :Type => Number, :Version => "1.4" field :AIS, :Type => Boolean, :Version => "1.4" field :TK, :Type => Boolean, :Version => "1.4" end end #module Graphics class PDF::Instruction insn 'q' do |canvas| canvas.gs.save; canvas.gs.reset end insn 'Q' do |canvas| canvas.gs.restore end insn 'w', Real do |canvas, lw| canvas.gs.line_width = lw end insn 'J', Real do |canvas, lc| canvas.gs.line_cap = lc end insn 'j', Real do |canvas, lj| canvas.gs.line_join = lj end insn 'M', Real do |canvas, ml| canvas.gs.miter_limit = ml end insn 'd', Array, Integer do |canvas, array, phase| canvas.gs.dash_pattern = Graphics::DashPattern.new array, phase end insn 'ri', Name do |canvas, ri| canvas.gs.rendering_intent = ri end end end origami-2.0.0/lib/origami/page.rb0000644000004100000410000006371312757133666016660 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end module Origami class PDF # # Appends a page or list of pages to the end of the page tree. # _page_:: The page to append to the document. Creates a new Page if not specified. # # Pass the Page object if a block is present. # def append_page(page = Page.new) unless self.Catalog and self.Catalog.Pages and self.Catalog.Pages.is_a?(PageTreeNode) raise InvalidPDFError, "Invalid page tree" end treeroot = self.Catalog.Pages treeroot.Kids ||= [] #:nodoc: treeroot.Kids.push(page) treeroot.Count ||= 0 treeroot.Count += 1 page.Parent = treeroot yield(page) if block_given? self end # # Inserts a page at position _index_ into the document. # _index_:: Page index (starting from zero). # _page_:: The page to insert into the document. Creates a new one if none given. # # Pass the Page object if a block is present. # def insert_page(index, page = Page.new) unless self.Catalog and self.Catalog.Pages and self.Catalog.Pages.is_a?(PageTreeNode) raise InvalidPageTreeError, "Invalid page tree" end # Page from another document must be exported. page = page.export if page.document and page.document != self self.Catalog.Pages.insert_page(index, page) yield(page) if block_given? self end # # Returns an Enumerator of Page # def pages unless self.Catalog and self.Catalog.Pages and self.Catalog.Pages.is_a?(PageTreeNode) raise InvalidPageTreeError, "Invalid page tree" end self.Catalog.Pages.pages end # # Iterate through each page, returns self. # def each_page(&b) unless self.Catalog and self.Catalog.Pages and self.Catalog.Pages.is_a?(PageTreeNode) raise InvalidPageTreeError, "Invalid page tree" end self.Catalog.Pages.each_page(&b) end # # Get the n-th Page object. # def get_page(n) unless self.Catalog and self.Catalog.Pages and self.Catalog.Pages.is_a?(PageTreeNode) raise InvalidPageTreeError, "Invalid page tree" end self.Catalog.Pages.get_page(n) end # # Lookup page in the page name directory. # def get_page_by_name(name) resolve_name Names::PAGES, name end # # Calls block for each named page. # def each_named_page(&b) each_name(Names::PAGES, &b) end end module ResourcesHolder def add_extgstate(extgstate, name = nil) add_resource(Resources::EXTGSTATE, extgstate, name) end def add_colorspace(colorspace, name = nil) add_resource(Resources::COLORSPACE, colorspace, name) end def add_pattern(pattern, name = nil) add_resource(Resources::PATTERN, pattern, name) end def add_shading(shading, name = nil) add_resource(Resources::SHADING, shading, name) end def add_xobject(xobject, name = nil) add_resource(Resources::XOBJECT, xobject, name) end def add_font(font, name = nil) add_resource(Resources::FONT, font, name) end def add_properties(properties, name = nil) add_resource(Resources::PROPERTIES, properties, name) end # # Adds a resource of the specified _type_ in the current object. # If _name_ is not specified, a new name will be automatically generated. # def add_resource(type, rsrc, name = nil) if name.nil? and existing = self.resources(type).key(rsrc) return existing end name = new_id(type) unless name target = self.is_a?(Resources) ? self : (self.Resources ||= Resources.new) rsrc_dict = (target[type] and target[type].solve) || (target[type] = Dictionary.new) rsrc_dict[name] = rsrc name end # # Iterates over the resources by _type_. # def each_resource(type) target = self.is_a?(Resources) ? self : (self.Resources ||= Resources.new) rsrc = target[type] and target[type].solve return enum_for(__method__, type) { rsrc.is_a?(Dictionary) ? rsrc.length : 0 } unless block_given? return unless rsrc.is_a?(Dictionary) rsrc.each_pair do |name, obj| yield(name.value, obj.solve) end end def each_colorspace(&block); each_resource(Resources::COLORSPACE, &block) end def each_extgstate(&block); each_resource(Resources::EXTGSTATE, &block) end def each_pattern(&block); each_resource(Resources::PATTERN, &block) end def each_shading(&block); each_resource(Resources::SHADING, &block) end def each_xobject(&block); each_resource(Resources::XOBJECT, &block) end def each_font(&block); each_resource(Resources::FONT, &block) end def each_property(&block); each_resource(Resources::PROPERTIES, &block) end def extgstates; each_extgstate.to_h end def colorspaces; each_colorspace.to_h end def patterns; each_pattern.to_h end def shadings; each_shading.to_h end def xobjects; each_xobject.to_h end def fonts; each_font.to_h end def properties; each_property.to_h end # # Returns a Hash of all resources in the object or only the specified _type_. # def resources(type = nil) if type.nil? self.extgstates .merge self.colorspaces .merge self.patterns .merge self.shadings .merge self.xobjects .merge self.fonts .merge self.properties else self.each_resource(type).to_h end end private def new_id(type, prefix = nil) #:nodoc: prefix ||= { Resources::EXTGSTATE => 'ExtG', Resources::COLORSPACE => 'CS', Resources::PATTERN => 'P', Resources::SHADING => 'Sh', Resources::XOBJECT => 'Im', Resources::FONT => 'F', Resources::PROPERTIES => 'Pr' }[type] rsrc = self.resources(type) n = '1' n.next! while rsrc.include?((prefix + n).to_sym) Name.new(prefix + n) end def new_extgstate_id; new_id(Resources::EXTGSTATE) end def new_colorspace_id; new_id(Resources::COLORSPACE) end def new_pattern_id; new_id(Resources::PATTERN) end def new_shading_id; new_id(Resources::SHADING) end def new_xobject_id; new_id(Resources::XOBJECT) end def new_font_id; new_name(Resources::FONT) end def new_properties_id; new_name(Resources::PROPERTIES) end end # # Class representing a Resources Dictionary for a Page. # class Resources < Dictionary include StandardObject include ResourcesHolder EXTGSTATE = :ExtGState COLORSPACE = :ColorSpace PATTERN = :Pattern SHADING = :Shading XOBJECT = :XObject FONT = :Font PROPERTIES = :Properties field EXTGSTATE, :Type => Dictionary field COLORSPACE, :Type => Dictionary field PATTERN, :Type => Dictionary field SHADING, :Type => Dictionary, :Version => "1.3" field XOBJECT, :Type => Dictionary field FONT, :Type => Dictionary field :ProcSet, :Type => Array.of(Name) field PROPERTIES, :Type => Dictionary, :Version => "1.2" def pre_build add_font(Font::Type1::Standard::Helvetica.new.pre_build) unless self.Font super end end class InvalidPageTreeError < Error #:nodoc: end # # Class representing a node in a Page tree. # class PageTreeNode < Dictionary include StandardObject field :Type, :Type => Name, :Default => :Pages, :Required => true field :Parent, :Type => PageTreeNode field :Kids, :Type => Array, :Default => [], :Required => true field :Count, :Type => Integer, :Default => 0, :Required => true def initialize(hash = {}, parser = nil) self.Count = 0 self.Kids = [] super(hash, parser) set_indirect(true) end def pre_build #:nodoc: self.Count = self.pages.count super end def insert_page(index, page) raise IndexError, "Invalid index for page tree" if index > self.Count count = 0 kids = self.Kids kids.length.times do |n| if count == index kids.insert(n, page) self.Count = self.Count + 1 page.Parent = self return self else node = kids[n].solve case node when Page count = count + 1 next when PageTreeNode if count + node.Count > index node.insert_page(index - count, page) self.Count = self.Count + 1 return self else count = count + node.Count next end end end end if count == index self << page else raise IndexError, "An error occured while inserting page" end self end # # Returns an Array of Pages inheriting this tree node. # def pages self.each_page.to_a end # # Iterate through each page of that node. # def each_page(browsed_nodes: [], &block) return enum_for(__method__) { self.Count.to_i } unless block_given? if browsed_nodes.any?{|node| node.equal?(self)} raise InvalidPageTreeError, "Cyclic tree graph detected" end unless self.Kids.is_a?(Array) raise InvalidPageTreeError, "Kids must be an Array" end browsed_nodes.push(self) unless self.Count.nil? [ self.Count.value, self.Kids.length ].min.times do |n| node = self.Kids[n].solve case node when PageTreeNode then node.each_page(browsed_nodes: browsed_nodes, &block) when Page then yield(node) else raise InvalidPageTreeError, "not a Page or PageTreeNode" end end end self end # # Get the n-th Page object in this node, starting from 1. # def get_page(n, browsed_nodes: []) raise IndexError, "Page numbers are referenced starting from 1" if n < 1 if browsed_nodes.any?{|node| node.equal?(self)} raise InvalidPageTreeError, "Cyclic tree graph detected" end unless self.Kids.is_a?(Array) raise InvalidPageTreeError, "Kids must be an Array" end decount = n [ self.Count.value, self.Kids.length ].min.times do |i| node = self.Kids[i].solve case node when Page decount = decount - 1 return node if decount == 0 when PageTreeNode nchilds = [ node.Count.value, node.Kids.length ].min if nchilds >= decount return node.get_page(decount, browsed_nodes: browsed_nodes) else decount -= nchilds end else raise InvalidPageTreeError, "not a Page or PageTreeNode" end end raise IndexError, "Page not found" end def << (pageset) pageset = [pageset] unless pageset.is_a?(::Array) unless pageset.all? {|item| item.is_a?(Page) or item.is_a?(PageTreeNode) } raise TypeError, "Cannot add anything but Page and PageTreeNode to this node" end self.Kids ||= Array.new self.Kids.concat(pageset) self.Count = self.Kids.length pageset.each do |node| node.Parent = self end end end class PageLabel < Dictionary include StandardObject module Style DECIMAL = :D UPPER_ROMAN = :R LOWER_ROMAN = :r UPPER_ALPHA = :A LOWER_ALPHA = :a end field :Type, :Type => Name, :Default => :PageLabel field :S, :Type => Name field :P, :Type => String field :St, :Type => Integer end # Forward declarations. class ContentStream < Stream; end class Annotation < Dictionary; end module Graphics; class ImageXObject < Stream; end end # # Class representing a Page in the PDF document. # class Page < Dictionary include StandardObject include ResourcesHolder class BoxStyle < Dictionary include StandardObject SOLID = :S DASH = :D field :C, :Type => Array.of(Number), :Default => [0.0, 0.0, 0.0] field :W, :Type => Number, :Default => 1 field :S, :Type => Name, :Default => SOLID field :D, :Type => Array.of(Integer) end # # Box color information dictionary associated to a Page. # class BoxColorInformation < Dictionary include StandardObject field :CropBox, :Type => BoxStyle field :BleedBox, :Type => BoxStyle field :TrimBox, :Type => BoxStyle field :ArtBox, :Type => BoxStyle end # # Class representing a navigation node associated to a Page. # class NavigationNode < Dictionary include StandardObject field :Type, :Type => Name, :Default => :NavNode field :NA, :Type => Dictionary # Next action field :PA, :Type => Dictionary # Prev action field :Next, :Type => NavigationNode field :Prev, :Type => NavigationNode field :Dur, :Type => Number end # # Class representing additional actions which can be associated to a Page. # class AdditionalActions < Dictionary include StandardObject field :O, :Type => Dictionary, :Version => "1.2" # Page Open field :C, :Type => Dictionary, :Version => "1.2" # Page Close end module Format A0 = Rectangle[width: 2384, height: 3370] A1 = Rectangle[width: 1684, height: 2384] A2 = Rectangle[width: 1191, height: 1684] A3 = Rectangle[width: 842, height: 1191] A4 = Rectangle[width: 595, height: 842] A5 = Rectangle[width: 420, height: 595] A6 = Rectangle[width: 298, height: 420] A7 = Rectangle[width: 210, height: 298] A8 = Rectangle[width: 147, height: 210] A9 = Rectangle[width: 105, height: 147] A10 = Rectangle[width: 74, height: 105] B0 = Rectangle[width: 2836, height: 4008] B1 = Rectangle[width: 2004, height: 2835] B2 = Rectangle[width: 1417, height: 2004] B3 = Rectangle[width: 1001, height: 1417] B4 = Rectangle[width: 709, height: 1001] B5 = Rectangle[width: 499, height: 709] B6 = Rectangle[width: 354, height: 499] B7 = Rectangle[width: 249, height: 354] B8 = Rectangle[width: 176, height: 249] B9 = Rectangle[width: 125, height: 176] B10 = Rectangle[width: 88, height: 125] end field :Type, :Type => Name, :Default => :Page, :Required => true field :Parent, :Type => PageTreeNode, :Required => true field :LastModified, :Type => String, :Version => "1.3" field :Resources, :Type => Resources, :Required => true field :MediaBox, :Type => Rectangle, :Default => Format::A4, :Required => true field :CropBox, :Type => Rectangle field :BleedBox, :Type => Rectangle, :Version => "1.3" field :TrimBox, :Type => Rectangle, :Version => "1.3" field :ArtBox, :Type => Rectangle, :Version => "1.3" field :BoxColorInfo, :Type => BoxColorInformation, :Version => "1.4" field :Contents, :Type => [ ContentStream, Array.of(ContentStream) ] field :Rotate, :Type => Integer, :Default => 0 field :Group, :Type => Dictionary, :Version => "1.4" field :Thumb, :Type => Graphics::ImageXObject field :B, :Type => Array, :Version => "1.1" field :Dur, :Type => Integer, :Version => "1.1" field :Trans, :Type => Dictionary, :Version => "1.1" field :Annots, :Type => Array.of(Annotation) field :AA, :Type => AdditionalActions, :Version => "1.2" field :Metadata, :Type => MetadataStream, :Version => "1.4" field :PieceInfo, :Type => Dictionary, :Version => "1.2" field :StructParents, :Type => Integer, :Version => "1.3" field :ID, :Type => String field :PZ, :Type => Number field :SeparationInfo, :Type => Dictionary, :Version => "1.3" field :Tabs, :Type => Name, :Version => "1.5" field :TemplateAssociated, :Type => Name, :Version => "1.5" field :PresSteps, :Type => NavigationNode, :Version => "1.5" field :UserUnit, :Type => Number, :Default => 1.0, :Version => "1.6" field :VP, :Type => Dictionary, :Version => "1.6" def initialize(hash = {}, parser = nil) super(hash, parser) set_indirect(true) end def pre_build self.Resources = Resources.new.pre_build unless self.has_key?(:Resources) super end # # Iterates over all the ContentStreams of the Page. # def each_content_stream contents = self.Contents return enum_for(__method__) do case contents when Array then contents.length when Stream then 1 else 0 end end unless block_given? case contents when Stream then yield(contents) when Array then contents.each { |stm| yield(stm.solve) } end end # # Returns an Array of ContentStreams for the Page. # def content_streams self.each_content_stream.to_a end # # Add an Annotation to the Page. # def add_annotation(*annotations) unless annotations.all?{|annot| annot.is_a?(Annotation) or annot.is_a?(Reference)} raise TypeError, "Only Annotation objects must be passed." end self.Annots ||= [] annotations.each do |annot| annot.solve[:P] = self if self.indirect? self.Annots << annot end end # # Iterate through each Annotation of the Page. # def each_annotation annots = self.Annots return enum_for(__method__) { annots.is_a?(Array) ? annots.length : 0 } unless block_given? return unless annots.is_a?(Array) annots.each do |annot| yield(annot.solve) end end # # Returns the array of Annotation objects of the Page. # def annotations self.each_annotation.to_a end # # Embed a SWF Flash application in the page. # def add_flash_application(swfspec, params = {}) options = { windowed: false, transparent: false, navigation_pane: false, toolbar: false, pass_context_click: false, activation: Annotation::RichMedia::Activation::PAGE_OPEN, deactivation: Annotation::RichMedia::Deactivation::PAGE_CLOSE, flash_vars: nil } options.update(params) annot = create_richmedia(:Flash, swfspec, options) add_annotation(annot) annot end # # Will execute an action when the page is opened. # def onOpen(action) unless action.is_a?(Action) or action.is_a?(Reference) raise TypeError, "An Action object must be passed." end self.AA ||= Page::AdditionalActions.new self.AA.O = action self end # # Will execute an action when the page is closed. # def onClose(action) unless action.is_a?(Action) or action.is_a?(Reference) raise TypeError, "An Action object must be passed." end self.AA ||= Page::AdditionalActions.new self.AA.C = action self end # # Will execute an action when navigating forward from this page. # def onNavigateForward(action) #:nodoc: unless action.is_a?(Action) or action.is_a?(Reference) raise TypeError, "An Action object must be passed." end self.PresSteps ||= NavigationNode.new self.PresSteps.NA = action self end # # Will execute an action when navigating backward from this page. # def onNavigateBackward(action) #:nodoc: unless action.is_a?(Action) or action.is_a?(Reference) raise TypeError, "An Action object must be passed." end self.PresSteps ||= NavigationNode.new self.PresSteps.PA = action self end private def create_richmedia(type, content, params) #:nodoc: content.set_indirect(true) richmedia = Annotation::RichMedia.new.set_indirect(true) rminstance = Annotation::RichMedia::Instance.new.set_indirect(true) rmparams = rminstance.Params = Annotation::RichMedia::Parameters.new rmparams.Binding = Annotation::RichMedia::Parameters::Binding::BACKGROUND rmparams.FlashVars = params[:flash_vars] rminstance.Asset = content rmconfig = Annotation::RichMedia::Configuration.new.set_indirect(true) rmconfig.Instances = [ rminstance ] rmconfig.Subtype = type rmcontent = richmedia.RichMediaContent = Annotation::RichMedia::Content.new.set_indirect(true) rmcontent.Assets = NameTreeNode.new rmcontent.Assets.Names = NameLeaf.new(content.F.value => content) rmcontent.Configurations = [ rmconfig ] rmsettings = richmedia.RichMediaSettings = Annotation::RichMedia::Settings.new rmactivation = rmsettings.Activation = Annotation::RichMedia::Activation.new rmactivation.Condition = params[:activation] rmactivation.Configuration = rmconfig rmactivation.Animation = Annotation::RichMedia::Animation.new(:PlayCount => -1, :Subtype => :Linear, :Speed => 1.0) rmpres = rmactivation.Presentation = Annotation::RichMedia::Presentation.new rmpres.Style = Annotation::RichMedia::Presentation::WINDOWED if params[:windowed] rmpres.Transparent = params[:transparent] rmpres.NavigationPane = params[:navigation_pane] rmpres.Toolbar = params[:toolbar] rmpres.PassContextClick = params[:pass_context_click] rmdeactivation = rmsettings.Deactivation = Annotation::RichMedia::Deactivation.new rmdeactivation.Condition = params[:deactivation] richmedia end end end origami-2.0.0/lib/origami/destinations.rb0000644000004100000410000001537612757133666020452 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end module Origami class PDF # # Lookup destination in the destination name directory. # def get_destination_by_name(name) resolve_name Names::DESTINATIONS, name end # # Calls block for each named destination. # def each_named_dest(&b) each_name(Names::DESTINATIONS, &b) end end # # A destination represents a specified location into the document. # class Destination < Origami::Array attr_reader :page, :top, :left, :right, :bottom, :zoom # # Class representing a Destination zooming on a part of a document. # class Zoom < Destination def initialize(array) super(array) @page, _, @left, @top, @zoom = array end # # Creates a new zoom Destination. # _page_:: The destination Page. # _left_, _top_:: Coords in the Page. # _zoom_:: Zoom factor. # def self.[](page, left: 0, top: 0, zoom: 0) self.new([page, :XYZ, left, top, zoom]) end end def self.Zoom(page, left: 0, top: 0, zoom: 0) Zoom[page, left: left, top: top, zoom: zoom] end # # Class representing a Destination showing a Page globally. # class GlobalFit < Destination def initialize(array) super(array) @page, _ = array end # # Creates a new global fit Destination. # _page_:: The destination Page. # def self.[](page) self.new([page, :Fit]) end end def self.GlobalFit(page) GlobalFit[page] end # # Class representing a Destination fitting a Page horizontally. # class HorizontalFit < Destination def initialize(array) super(array) @page, _, @top = array end # # Creates a new horizontal fit destination. # _page_:: The destination Page. # _top_:: The vertical coord in the Page. # def self.[](page, top: 0) self.new([page, :FitH, top]) end end def self.HorizontalFit(page, top: 0) HorizontalFit[page, top: top] end # # Class representing a Destination fitting a Page vertically. # _page_:: The destination Page. # _left_:: The horizontal coord in the Page. # class VerticalFit < Destination def initialize(array) super(array) @page, _, @left = array end def self.[](page, left: 0) self.new([page, :FitV, left]) end end def self.VerticalFit(page, left: 0) VerticalFit[page, left: left] end # # Class representing a Destination fitting the view on a rectangle in a Page. # class RectangleFit < Destination def initialize(array) super(array) @page, _, @left, @bottom, @right, @top = array end # # Creates a new rectangle fit Destination. # _page_:: The destination Page. # _left_, _bottom_, _right_, _top_:: The rectangle to fit in. # def self.[](page, left: 0, bottom: 0, right: 0, top: 0) self.new([pageref, :FitR, left, bottom, right, top]) end end def self.RectangleFit(page, left: 0, bottom: 0, right: 0, top: 0) RectangleFit[page, left: left, bottom: bottom, right: right, top: top] end # # Class representing a Destination fitting the bounding box of a Page. # class GlobalBoundingBoxFit < Destination def initialize(array) super(array) @page, _, = array end # # Creates a new bounding box fit Destination. # _page_:: The destination Page. # def self.[](page) self.new([page, :FitB]) end end def self.GlobalBoundingBoxFit(page) GlobalBoundingBoxFit[page] end # # Class representing a Destination fitting horizontally the bouding box a Page. # class HorizontalBoudingBoxFit < Destination def initialize(array) super(array) @page, _, @top = array end # # Creates a new horizontal bounding box fit Destination. # _page_:: The destination Page. # _top_:: The vertical coord. # def self.[](page, top: 0) self.new([page, :FitBH, top]) end end def self.HorizontalBoudingBoxFit(page, top: 0) HorizontalBoudingBoxFit[page, top: top] end # # Class representing a Destination fitting vertically the bounding box of a Page. # class VerticalBoundingBoxFit < Destination def initialize(array) super(array) @page, _, @left = array end # # Creates a new vertical bounding box fit Destination. # _page_:: The destination Page. # _left_:: The horizontal coord. # def self.[](page, left: 0) self.new([pageref, :FitBV, left]) end end def self.VerticalBoundingBoxFit(page, left: 0) VerticalBoundingBoxFit[page, left: left] end end # # This kind of Dictionary is used in named destinations. # class DestinationDictionary < Dictionary include StandardObject field :D, :Type => Destination, :Required => true end end origami-2.0.0/lib/origami/numeric.rb0000644000004100000410000001077312757133666017404 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end require 'delegate' module Origami class InvalidIntegerObjectError < InvalidObjectError #:nodoc: end # # Class representing a PDF number (Integer, or Real). # module Number include Origami::Object def ~ self.class.new(~self.to_i) end def |(val) self.class.new(self.to_i | val) end def &(val) self.class.new(self.to_i & val) end def ^(val) self.class.new(self.to_i ^ val) end def <<(val) self.class.new(self.to_i << val) end def >>(val) self.class.new(self.to_i >> val) end def +(val) self.class.new(self.to_i + val) end def -(val) self.class.new(self.to_i - val) end def -@ self.class.new(-self.to_i) end def *(val) self.class.new(self.to_i * val) end def /(val) self.class.new(self.to_i / val) end def abs self.class.new(self.to_i.abs) end def **(val) self.class.new(self.to_i ** val) end def to_s super(value.to_s) end module ClassMethods #:nodoc:all def native_type; Number end end def self.included(receiver) #:nodoc: receiver.extend(ClassMethods) end def self.native_type; Number end #:nodoc: end # # Class representing an Integer Object. # class Integer < DelegateClass(Bignum) include Number TOKENS = [ "(\\+|-)?[\\d]+[^.]?" ] #:nodoc: REGEXP_TOKEN = Regexp.new(TOKENS.first) @@regexp = Regexp.new(WHITESPACES + "(?(\\+|-)?[\\d]+)") # # Creates a new Integer from a Ruby Fixnum / Bignum. # _i_:: The Integer value. # def initialize(i = 0) unless i.is_a?(::Integer) raise TypeError, "Expected type Fixnum or Bignum, received #{i.class}." end super(i) end def self.parse(stream, parser = nil) #:nodoc: offset = stream.pos if not stream.scan(@@regexp) raise InvalidIntegerObjectError, "Invalid integer format" end value = stream['int'].to_i int = Integer.new(value) int.file_offset = offset int end alias value to_i end class InvalidRealObjectError < InvalidObjectError #:nodoc: end # # Class representing a Real number Object. # PDF real numbers are arbitrary precision numbers, depending on architectures. # class Real < DelegateClass(Float) include Number TOKENS = [ "(\\+|-)?([\\d]*\\.[\\d]+|[\\d]+\\.[\\d]*)([eE](\\+|-)?[\\d]+)?" ] #:nodoc: REGEXP_TOKEN = Regexp.new(TOKENS.first) @@regexp = Regexp.new(WHITESPACES + "(?#{TOKENS.first})") # # Creates a new Real from a Ruby Float. # _f_:: The new Real value. # def initialize(f = 0) unless f.is_a?(Float) raise TypeError, "Expected type Float, received #{f.class}." end super(f) end def self.parse(stream, parser = nil) #:nodoc: offset = stream.pos if not stream.scan(@@regexp) raise InvalidRealObjectError, "Invalid real number format" end value = stream['real'].to_f real = Real.new(value) real.file_offset = offset real end alias value to_f def to_s sprintf("%f", self).sub(/\.0*$|(\.\d*[^0])0*$/, '\1') end end end origami-2.0.0/lib/origami/extensions/0000755000004100000410000000000012757133666017604 5ustar www-datawww-dataorigami-2.0.0/lib/origami/extensions/ppklite.rb0000644000004100000410000003053612757133666021610 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end require 'origami/object' require 'origami/name' require 'origami/dictionary' require 'origami/reference' require 'origami/boolean' require 'origami/numeric' require 'origami/string' require 'origami/array' require 'origami/trailer' require 'origami/xreftable' require 'origami/parsers/ppklite' require 'openssl' module Origami # # Class representing an Adobe Reader certificate store. # class PPKLite class Error < Origami::Error; end def self.read(path, options = {}) path = File.expand_path(path) if path.is_a?(::String) PPKLite::Parser.new(options).parse(path) end # # Class representing a certificate store header. # class Header MAGIC = /%PPKLITE-(?\d)\.(?\d)/ attr_accessor :major_version, :minor_version # # Creates a file header, with the given major and minor versions. # _major_version_:: Major version. # _minor_version_:: Minor version. # def initialize(major_version = 2, minor_version = 1) @major_version, @minor_version = major_version, minor_version end def self.parse(stream) #:nodoc: if not stream.scan(MAGIC).nil? maj = stream['major'].to_i min = stream['minor'].to_i else raise InvalidHeader, "Invalid header format" end stream.skip(REGEXP_WHITESPACES) PPKLite::Header.new(maj, min) end # # Outputs self into PDF code. # def to_s "%PPKLITE-#{@major_version}.#{@minor_version}".b + EOL end def to_sym #:nodoc: "#{@major_version}.#{@minor_version}".to_sym end def to_f #:nodoc: to_sym.to_s.to_f end end class Revision #:nodoc; attr_accessor :document attr_accessor :body, :xreftable attr_reader :trailer def initialize(adbk) @document = adbk @body = {} @xreftable = nil @trailer = nil end def trailer=(trl) trl.document = @document @trailer = trl end def each_object(&b) @body.each_value(&b) end def objects @body.values end end module Descriptor CERTIFICATE = 1 USER = 2 def self.included(receiver) #:nodoc: receiver.field :ID, :Type => Integer, :Required => true receiver.field :ABEType, :Type => Integer, :Default => Descriptor::CERTIFICATE, :Required => true end end class Certificate < Dictionary include StandardObject include Descriptor add_type_info self, :ABEType, Descriptor::CERTIFICATE module Flags CAN_CERTIFY = 1 << 1 ALLOW_DYNAMIC_CONTENT = 1 << 2 UNKNOWN_1 = 1 << 3 ALLOW_HIGH_PRIV_JS = 1 << 4 UNKNOWN_2 = 1 << 5 IS_ROOT_CA = 1 << 6 #FULL_TRUST = 1 << 1 | 1 << 2 | 1 << 3 | 1 << 4 | 1 << 5 | 1 << 6 FULL_TRUST = 8190 end field :ABEType, :Type => Integer, :Default => Descriptor::CERTIFICATE, :Required => true field :Usage, :Type => Integer, :Default => 1, :Required => true field :Viewable, :Type => Boolean, :Default => true field :Editable, :Type => Boolean, :Default => true field :Cert, :Type => String, :Required => true field :Trust, :Type => Integer, :Default => Flags::UNKNOWN_2, :Required => true end class User < Dictionary include StandardObject include Descriptor add_type_info self, :ABEType, Descriptor::USER field :ABEType, :Type => Integer, :Default => Descriptor::USER, :Required => true field :Name, :Type => String, :Required => true field :Encrypt, :Type => Integer field :Certs, :Type => Array.of(Certificate), :Default => [], :Required => true end class AddressList < Dictionary include StandardObject field :Type, :Type => Name, :Default => :AddressBook, :Required => true field :NextID, :Type => Integer field :Entries, :Type => Array.of(Descriptor), :Default => [], :Required => true end class UserList < Dictionary include StandardObject field :Type, :Type => Name, :Default => :User, :Required => true end class PPK < Dictionary include StandardObject field :Type, :Type => Name, :Default => :PPK, :Required => true field :User, :Type => UserList, :Required => true field :AddressBook, :Type => AddressList, :Required => true field :V, :Type => Integer, :Default => 0x10001, :Required => true end class Catalog < Dictionary include StandardObject field :Type, :Type => Name, :Default => :Catalog, :Required => true field :PPK, :Type => PPK, :Required => true end attr_accessor :header, :revisions def initialize(parser = nil) #:nodoc: @header = PPKLite::Header.new @revisions = [ Revision.new(self) ] @revisions.first.trailer = Trailer.new init if parser.nil? end def indirect_objects @revisions.inject([]) do |set, rev| set.concat(rev.objects) end end alias root_objects indirect_objects def cast_object(reference, type, parser = nil) #:nodoc: @revisions.each do |rev| if rev.body.include?(reference) and type < rev.body[reference].class rev.body[reference] = rev.body[reference].cast_to(type, parser) rev.body[reference] else nil end end end def get_object(no, generation = 0) #:nodoc: case no when Reference target = no when ::Integer target = Reference.new(no, generation) when Origami::Object return no end @revisions.first.body[target] end def <<(object) object.set_indirect(true) object.set_document(self) if object.no.zero? maxno = 1 maxno = maxno.succ while get_object(maxno) object.generation = 0 object.no = maxno end @revisions.first.body[object.reference] = object object.reference end alias insert << def Catalog get_object(@revisions.first.trailer.Root) end def indirect_objects @revisions.inject([]) do |set, rev| set.concat(rev.objects) end end alias root_objects indirect_objects def save(path) bin = "".b bin << @header.to_s lastno, brange = 0, 0 xrefs = [ XRef.new(0, XRef::FIRSTFREE, XRef::FREE) ] xrefsection = XRef::Section.new @revisions.first.body.values.sort.each { |obj| if (obj.no - lastno).abs > 1 xrefsection << XRef::Subsection.new(brange, xrefs) brange = obj.no xrefs.clear end xrefs << XRef.new(bin.size, obj.generation, XRef::USED) lastno = obj.no obj.pre_build bin << obj.to_s obj.post_build } xrefsection << XRef::Subsection.new(brange, xrefs) @xreftable = xrefsection @trailer ||= Trailer.new @trailer.Size = @revisions.first.body.size + 1 @trailer.startxref = bin.size bin << @xreftable.to_s bin << @trailer.to_s if path.respond_to?(:write) io = path else path = File.expand_path(path) io = File.open(path, "wb", encoding: 'binary') close = true end begin io.write(bin) ensure io.close if close end self end def each_user(&b) each_entry(Descriptor::USER, &b) end def get_user(id) self.each_user.find {|user| user.ID == id } end def users self.each_user.to_a end def each_certificate(&b) each_entry(Descriptor::CERTIFICATE, &b) end def get_certificate(id) self.each_certificate.find {|cert| cert.ID == id } end def certificates self.each_certificate.to_a end # # Add a certificate into the address book # def add_certificate(certfile, attributes, viewable: false, editable: false) if certfile.is_a?(OpenSSL::X509::Certificate) x509 = certfile else x509 = OpenSSL::X509::Certificate.new(certfile) end address_book = get_address_book cert = Certificate.new cert.Cert = x509.to_der cert.ID = address_book.NextID address_book.NextID += 1 cert.Trust = attributes cert.Viewable = viewable cert.Editable = editable address_book.Entries.push(self << cert) end private def init catalog = Catalog.new( PPK: PPK.new( User: UserList.new, AddressBook: AddressList.new( Entries: [], NextID: 1 ) ) ) @revisions.first.trailer.Root = self.insert(catalog) end def each_entry(type) return enum_for(__method__, type) unless block_given? address_book = get_address_book address_book.Entries.each do |entry| entry = entry.solve yield(entry) if entry.is_a?(Dictionary) and entry.ABEType == type end end def get_address_book raise Error, "Broken catalog" unless self.Catalog.is_a?(Dictionary) and self.Catalog.PPK.is_a?(Dictionary) ppk = self.Catalog.PPK raise Error, "Broken PPK" unless ppk.AddressBook.is_a?(Dictionary) address_book = ppk.AddressBook raise Error, "Broken address book" unless address_book.Entries.is_a?(Array) address_book end def get_object_offset(no,generation) #:nodoc: bodyoffset = @header.to_s.size objectoffset = bodyoffset @revisions.first.body.values.each { |object| if object.no == no and object.generation == generation then return objectoffset else objectoffset += object.to_s.size end } nil end end end origami-2.0.0/lib/origami/extensions/fdf.rb0000644000004100000410000002506012757133666020673 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end require 'origami/object' require 'origami/name' require 'origami/dictionary' require 'origami/reference' require 'origami/boolean' require 'origami/numeric' require 'origami/string' require 'origami/array' require 'origami/trailer' require 'origami/xreftable' require 'origami/parsers/fdf' module Origami # # Class representing an AcroForm Forms Data Format file. # class FDF def self.read(path, options = {}) path = File.expand_path(path) if path.is_a?(::String) FDF::Parser.new(options).parse(path) end class Header MAGIC = /%FDF-(?\d)\.(?\d)/ attr_accessor :major_version, :minor_version # # Creates a file header, with the given major and minor versions. # _major_version_:: Major version. # _minor_version_:: Minor version. # def initialize(major_version = 1, minor_version = 2) @major_version, @minor_version = major_version, minor_version end def self.parse(stream) #:nodoc: if not stream.scan(MAGIC).nil? maj = stream['major'].to_i min = stream['minor'].to_i else raise InvalidHeader, "Invalid header format" end stream.skip(REGEXP_WHITESPACES) FDF::Header.new(maj, min) end def to_s "%FDF-#{@major_version}.#{@minor_version}".b + EOL end def to_sym #:nodoc: "#{@major_version}.#{@minor_version}".to_sym end def to_f #:nodoc: to_sym.to_s.to_f end end class Revision #:nodoc; attr_accessor :document attr_accessor :body, :xreftable attr_reader :trailer def initialize(fdf) @document = fdf @body = {} @xreftable = nil @trailer = nil end def trailer=(trl) trl.document = @document @trailer = trl end def each_object(&b) @body.each_value(&b) end def objects @body.values end end class JavaScript < Dictionary include StandardObject field :Before, :Type => [ String, Stream ] field :After, :Type => [ String, Stream ] field :AfterPermsReady, :Type => [ String, Stream ] field :Doc, :Type => Array.of(Name, String) end class IconFit < Dictionary include StandardObject ALWAYS_SCALE = :A SCALE_WHEN_BIGGER = :B SCALE_WHEN_SMALLER = :S NEVER_SCALE = :N field :SW, :Type => Name field :S, :Type => Name field :A, :Type => Array.of(Number, length: 2) field :FB, :Type => Boolean end class NamedPageReference < Dictionary include StandardObject field :Name, :Type => String, :Required => true field :F, :Type => FileSpec end class Field < Dictionary include StandardObject field :Kids, :Type => Array.of(Field) field :T, :Type => String, :Required => true field :V, :Type => Dictionary field :Ff, :Type => Integer field :SetFf, :Type => Integer field :ClrFf, :Type => Integer field :F, :Type => Integer field :SetF, :Type => Integer field :ClrF, :Type => Integer field :AP, :Type => Annotation::AppearanceDictionary field :APRef, :Type => Dictionary field :IF, :Type => IconFit field :Opt, :Type => Array.of([String, Array.of(String, String)]) field :A, :Type => Action field :AA, :Type => Annotation::AdditionalActions field :RV, :Type => [ String, Stream ] end class Template < Dictionary include StandardObject field :TRef, :Type => NamedPageReference, :Required => true field :Fields, :Type => Array.of(Field) field :Rename, :Type => Boolean end class Page < Dictionary include StandardObject field :Templates, :Type => Array.of(Template), :Required => true field :Info, :Type => Dictionary end class Annotation < Origami::Annotation field :Page, :Type => Integer, :Required => true end class Dictionary < Origami::Dictionary include StandardObject field :F, :Type => FileSpec field :ID, :Type => Array.of(String, length: 2) field :Fields, :Type => Array.of(FDF::Field) field :Status, :Type => String field :Pages, :Type => Array.of(FDF::Page) field :Encoding, :Type => Name field :Annots, :Type => Array.of(FDF::Annotation) field :Differences, :Type => Stream field :Target, :Type => String field :EmbeddedFDFs, :Type => Array.of(FileSpec) field :JavaScript, :Type => JavaScript end class Catalog < Dictionary include StandardObject field :Version, :Type => Name field :FDF, :Type => FDF::Dictionary, :Required => true end attr_accessor :header, :revisions def initialize(parser = nil) #:nodoc: @header = FDF::Header.new @revisions = [ Revision.new(self) ] @revisions.first.trailer = Trailer.new init if parser.nil? end def <<(object) object.set_indirect(true) object.set_document(self) if object.no.zero? maxno = 1 maxno = maxno.succ while get_object(maxno) object.generation = 0 object.no = maxno end @revisions.first.body[object.reference] = object object.reference end alias insert << def get_object(no, generation = 0) #:nodoc: case no when Reference target = no when ::Integer target = Reference.new(no, generation) when Origami::Object return no end @revisions.first.body[target] end def indirect_objects @revisions.inject([]) do |set, rev| set.concat(rev.objects) end end alias root_objects indirect_objects def cast_object(reference, type, parser = nil) #:nodoc: @revisions.each do |rev| if rev.body.include?(reference) and type < rev.body[reference].class rev.body[reference] = rev.body[reference].cast_to(type, parser) rev.body[reference] else nil end end end def Catalog get_object(@revisions.first.trailer.Root) end def save(path) bin = "".b bin << @header.to_s lastno, brange = 0, 0 xrefs = [ XRef.new(0, XRef::FIRSTFREE, XRef::FREE) ] xrefsection = XRef::Section.new @revisions.first.body.values.sort.each { |obj| if (obj.no - lastno).abs > 1 xrefsection << XRef::Subsection.new(brange, xrefs) brange = obj.no xrefs.clear end xrefs << XRef.new(bin.size, obj.generation, XRef::USED) lastno = obj.no obj.pre_build bin << obj.to_s obj.post_build } xrefsection << XRef::Subsection.new(brange, xrefs) @xreftable = xrefsection @trailer ||= Trailer.new @trailer.Size = @revisions.first.body.size + 1 @trailer.startxref = bin.size bin << @xreftable.to_s bin << @trailer.to_s if path.respond_to?(:write) io = path else path = File.expand_path(path) io = File.open(path, "wb", encoding: 'binary') close = true end begin io.write(bin) ensure io.close if close end self end private def init catalog = Catalog.new(:FDF => FDF::Dictionary.new) @revisions.first.trailer.Root = self.insert(catalog) end def get_object_offset(no,generation) #:nodoc: bodyoffset = @header.to_s.size objectoffset = bodyoffset @revisions.first.body.values.each { |object| if object.no == no and object.generation == generation then return objectoffset else objectoffset += object.to_s.size end } nil end end end origami-2.0.0/lib/origami/stream.rb0000644000004100000410000005115112757133666017230 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end require 'strscan' module Origami class InvalidStreamObjectError < InvalidObjectError #:nodoc: end # Forward declaration. class FileSpec < Dictionary; end # # Class representing a PDF Stream Object. # Streams can be used to hold any kind of data, especially binary data. # class Stream include Origami::Object include StandardObject using TypeConversion TOKENS = [ "stream" + WHITECHARS_NORET + "\\r?\\n", "endstream" ] #:nodoc: @@regexp_open = Regexp.new(WHITESPACES + TOKENS.first) @@regexp_close = Regexp.new(TOKENS.last) @@cast_fingerprints = {} # # Actually only 5 first ones are implemented, # other ones are mainly about image data processing (JPEG, JPEG2000 ...) # @@defined_filters = %i[ ASCIIHexDecode ASCII85Decode LZWDecode FlateDecode RunLengthDecode CCITTFaxDecode JBIG2Decode DCTDecode JPXDecode AHx A85 LZW Fl RL CCF DCT ] attr_reader :dictionary field :Length, :Type => Integer, :Required => true field :Filter, :Type => [ Name, Array.of(Name) ] field :DecodeParms, :Type => [ Dictionary, Array.of(Dictionary) ] field :F, :Type => FileSpec, :Version => "1.2" field :FFilter, :Type => [ Name, Array.of(Name) ], :Version => "1.2" field :FDecodeParms, :Type => [ Dictionary, Array.of(Dictionary) ], :Version => "1.2" field :DL, :Type => Integer, :Version => "1.5" # # Creates a new PDF Stream. # _data_:: The Stream uncompressed data. # _dictionary_:: A hash representing the Stream attributes. # def initialize(data = "", dictionary = {}) super() set_indirect(true) @encoded_data = nil @dictionary, @data = Dictionary.new(dictionary), data @dictionary.parent = self end def dictionary=(dict) @dictionary = dict @dictionary.parent = self @dictionary end def pre_build encode! super end def post_build self.Length = @encoded_data.length super end def self.parse(stream, parser = nil) #:nodoc: dictionary = Dictionary.parse(stream, parser) return dictionary if not stream.skip(@@regexp_open) length = dictionary[:Length] if not length.is_a?(Integer) raw_data = stream.scan_until(@@regexp_close) if raw_data.nil? raise InvalidStreamObjectError, "Stream shall end with a 'endstream' statement" end else length = length.value raw_data = stream.peek(length) stream.pos += length if not ( unmatched = stream.scan_until(@@regexp_close) ) raise InvalidStreamObjectError, "Stream shall end with a 'endstream' statement" end raw_data << unmatched end stm = if Origami::OPTIONS[:enable_type_guessing] self.guess_type(dictionary).new('', dictionary.to_h) else Stream.new('', dictionary.to_h) end raw_data.chomp!(TOKENS.last) if raw_data[-1,1] == "\n" if raw_data[-2,1] == "\r" raw_data = raw_data[0, raw_data.size - 2] else raw_data = raw_data[0, raw_data.size - 1] end end #raw_data.chomp! if length.is_a?(Integer) and length < raw_data.length stm.encoded_data = raw_data stm.file_offset = dictionary.file_offset stm end def self.add_type_info(typeclass, key, value) #:nodoc: if not @@cast_fingerprints.has_key?(typeclass) and typeclass.superclass != Stream and @@cast_fingerprints.has_key?(typeclass.superclass) @@cast_fingerprints[typeclass] = @@cast_fingerprints[typeclass.superclass].dup end @@cast_fingerprints[typeclass] ||= {} @@cast_fingerprints[typeclass][key.to_o] = value.to_o end def self.guess_type(hash) #:nodoc: best_type = self @@cast_fingerprints.each_pair do |klass, keys| next unless klass < best_type best_type = klass if keys.all? { |k,v| hash[k] == v } end best_type end # # Iterates over each Filter in the Stream. # def each_filter filters = self.Filter return enum_for(__method__) do case filters when NilClass then 0 when Array then filters.length else 1 end end unless block_given? return if filters.nil? if filters.is_a?(Array) filters.each do |filter| yield(filter) end else yield(filters) end self end # # Returns an Array of Filters for this Stream. # def filters self.each_filter.to_a end # # Set predictor type for the current Stream. # Applies only for LZW and FlateDecode filters. # def set_predictor(predictor, colors: 1, bitspercomponent: 8, columns: 1) filters = self.filters unless filters.include?(:FlateDecode) or filters.include?(:LZWDecode) raise InvalidStreamObjectError, 'Predictor functions can only be used with Flate or LZW filters' end layer = filters.index(:FlateDecode) or filters.index(:LZWDecode) params = Filter::LZW::DecodeParms.new params[:Predictor] = predictor params[:Colors] = colors if colors != 1 params[:BitsPerComponent] = bitspercomponent if bitspercomponent != 8 params[:Columns] = columns if columns != 1 set_decode_params(layer, params) self end def cast_to(type, _parser = nil) super(type) cast = type.new("", self.dictionary.to_h) cast.encoded_data = self.encoded_data.dup cast.no, cast.generation = self.no, self.generation cast.set_indirect(true) cast.set_document(self.document) cast.file_offset = self.file_offset cast end def value #:nodoc: self end # # Returns the uncompressed stream content. # def data self.decode! unless self.decoded? @data end alias decoded_data data # # Sets the uncompressed stream content. # _str_:: The new uncompressed data. # def data=(str) @encoded_data = nil @data = str end alias decoded_data= data= # # Returns the raw compressed stream content. # def encoded_data self.encode! unless self.encoded? @encoded_data end # # Sets the raw compressed stream content. # _str_:: the new raw data. # def encoded_data=(str) @encoded_data = str @data = nil end # # Uncompress the stream data. # def decode! self.decrypt! if self.is_a?(Encryption::EncryptedStream) return if decoded? filters = self.filters if filters.empty? @data = @encoded_data.dup return end unless filters.all?{|filter| filter.is_a?(Name)} raise InvalidStreamObjectError, "Invalid Filter type parameter" end dparams = decode_params @data = @encoded_data.dup @data.freeze filters.each_with_index do |filter, layer| params = dparams[layer].is_a?(Dictionary) ? dparams[layer] : {} # Handle Crypt filters. if filter == :Crypt raise Filter::Error, "Crypt filter must be the first filter" unless layer.zero? # Skip the Crypt filter. next end begin @data = decode_data(@data, filter, params) rescue Filter::Error => error @data = error.decoded_data if error.decoded_data raise end end self end # # Compress the stream data. # def encode! return if encoded? filters = self.filters if filters.empty? @encoded_data = @data.dup return end unless filters.all?{|filter| filter.is_a?(Name)} raise InvalidStreamObjectError, "Invalid Filter type parameter" end dparams = decode_params @encoded_data = @data.dup (filters.length - 1).downto(0) do |layer| params = dparams[layer].is_a?(Dictionary) ? dparams[layer] : {} filter = filters[layer] # Handle Crypt filters. if filter == :Crypt raise Filter::Error, "Crypt filter must be the first filter" unless layer.zero? # Skip the Crypt filter. next end @encoded_data = encode_data(@encoded_data, filter, params) end self.Length = @encoded_data.length self end def to_s(indent: 1, tab: "\t") #:nodoc: content = "" content << @dictionary.to_s(indent: indent, tab: tab) content << "stream" + EOL content << self.encoded_data content << EOL << TOKENS.last super(content) end def [](key) #:nodoc: @dictionary[key] end def []=(key,val) #:nodoc: @dictionary[key] = val end def each_key(&b) #:nodoc: @dictionary.each_key(&b) end def key?(name) @dictionary.key?(name) end alias has_key? key? def self.native_type ; Stream end private def method_missing(field, *args) #:nodoc: if field.to_s[-1,1] == '=' self[field.to_s[0..-2].to_sym] = args.first else obj = self[field]; obj.is_a?(Reference) ? obj.solve : obj end end def decoded? #:nodoc: not @data.nil? end def encoded? #:nodoc: not @encoded_data.nil? end def each_decode_params params = self.DecodeParms return enum_for(__method__) do case params when NilClass then 0 when Array then params.length else 1 end end unless block_given? return if params.nil? if params.is_a?(Array) params.each do |param| yield(param) end else yield(params) end self end def decode_params each_decode_params.to_a end def set_decode_params(layer, params) #:nodoc: dparms = self.DecodeParms unless dparms.is_a? ::Array @dictionary[:DecodeParms] = dparms = [] end if layer > dparms.length - 1 dparms.concat(::Array.new(layer - dparms.length + 1, Null.new)) end dparms[layer] = params @dictionary[:DecodeParms] = dparms.first if dparms.length == 1 self end def decode_data(data, filter, params) #:nodoc: unless @@defined_filters.include?(filter.value) raise InvalidStreamObjectError, "Unknown filter : #{filter}" end Origami::Filter.const_get(filter.value.to_s.sub(/Decode$/,"")).decode(data, params) end def encode_data(data, filter, params) #:nodoc: unless @@defined_filters.include?(filter.value) raise InvalidStreamObjectError, "Unknown filter : #{filter}" end encoded = Origami::Filter.const_get(filter.value.to_s.sub(/Decode$/,"")).encode(data, params) if filter.value == :ASCIIHexDecode or filter.value == :ASCII85Decode encoded << Origami::Filter.const_get(filter.value.to_s.sub(/Decode$/,""))::EOD end encoded end end # # Class representing an external Stream. # class ExternalStream < Stream def initialize(filespec, hash = {}) hash[:F] = filespec super('', hash) end end class InvalidObjectStreamObjectError < InvalidStreamObjectError #:nodoc: end # # Class representing a Stream containing other Objects. # class ObjectStream < Stream include Enumerable NUM = 0 #:nodoc: OBJ = 1 #:nodoc: field :Type, :Type => Name, :Default => :ObjStm, :Required => true, :Version => "1.5" field :N, :Type => Integer, :Required => true field :First, :Type => Integer, :Required => true field :Extends, :Type => ObjectStream # # Creates a new Object Stream. # _dictionary_:: A hash of attributes to set to the Stream. # _raw_data_:: The Stream data. # def initialize(raw_data = "", dictionary = {}) super @objects = nil end def pre_build #:nodoc: load! if @objects.nil? prolog = "" data = "" objoff = 0 @objects.to_a.sort.each do |num,obj| obj.set_indirect(false) obj.objstm_offset = objoff prolog << "#{num} #{objoff} " objdata = "#{obj.to_s} " objoff += objdata.size data << objdata obj.set_indirect(true) obj.no = num end self.data = prolog + data @dictionary[:N] = @objects.size @dictionary[:First] = prolog.size super end # # Adds a new Object to this Stream. # _object_:: The Object to append. # def <<(object) unless object.generation == 0 raise InvalidObjectError, "Cannot store an object with generation > 0 in an ObjectStream" end if object.is_a?(Stream) raise InvalidObjectError, "Cannot store a Stream in an ObjectStream" end # We must have an associated document to generate new object numbers. if @document.nil? raise InvalidObjectError, "The ObjectStream must be added to a document before inserting objects" end # The object already belongs to a document. unless (obj_doc = object.document).nil? # Remove the previous instance if the object is indirect to avoid duplicates. if obj_doc.equal?(@document) @document.delete_object(object.reference) if object.indirect? else object = object.export end end load! if @objects.nil? object.no, object.generation = @document.allocate_new_object_number if object.no == 0 object.set_indirect(true) # object is indirect object.parent = self # set this stream as the parent object.set_document(@document) # indirect objects need pdf information @objects[object.no] = object Reference.new(object.no, 0) end alias insert << # # Deletes Object _no_. # def delete(no) load! if @objects.nil? @objects.delete(no) end # # Returns the index of Object _no_. # def index(no) ind = 0 @objects.to_a.sort.each do |num, obj| return ind if num == no ind = ind + 1 end nil end # # Returns a given decompressed object contained in the Stream. # _no_:: The Object number. # def extract(no) load! if @objects.nil? @objects[no] end # # Returns a given decompressed object by index. # _index_:: The Object index in the ObjectStream. # def extract_by_index(index) load! if @objects.nil? raise TypeError, "index must be an integer" unless index.is_a?(::Integer) raise IndexError, "index #{index} out of range" if index < 0 or index >= @objects.size @objects.to_a.sort[index][1] end # # Returns whether a specific object is contained in this stream. # _no_:: The Object number. # def include?(no) load! if @objects.nil? @objects.include?(no) end # # Iterates over each object in the stream. # def each(&b) load! if @objects.nil? @objects.values.each(&b) end alias each_object each # # Returns the number of objects contained in the stream. # def length raise InvalidObjectStreamObjectError, "Invalid number of objects" unless self.N.is_a?(Integer) self.N.to_i end # # Returns the array of inner objects. # def objects load! if @objects.nil? @objects.values end private def load! #:nodoc: decode! @objects = {} return if @data.empty? data = StringScanner.new(@data) nums = [] offsets = [] first_offset = first_object_offset self.length.times do nums << Integer.parse(data).to_i offsets << Integer.parse(data).to_i end self.length.times do |i| unless (0...@data.size).cover? (first_object_offset + offsets[i]) and offsets[i] >= 0 raise InvalidObjectStreamObjectError, "Invalid offset '#{offsets[i]} for object #{nums[i]}" end data.pos = first_offset + offsets[i] type = Object.typeof(data) raise InvalidObjectStreamObjectError, "Bad embedded object format in object stream" if type.nil? embeddedobj = type.parse(data) embeddedobj.set_indirect(true) # object is indirect embeddedobj.no = nums[i] # object number embeddedobj.parent = self # set this stream as the parent embeddedobj.set_document(@document) # indirect objects need pdf information embeddedobj.objstm_offset = offsets[i] @objects[nums[i]] = embeddedobj end end def first_object_offset #:nodoc: raise InvalidObjectStreamObjectError, "Invalid First offset" unless self.First.is_a?(Integer) raise InvalidObjectStreamObjectError, "Negative object offset" if self.First < 0 return self.First.to_i end end end origami-2.0.0/lib/origami/graphics.rb0000644000004100000410000000231312757133666017531 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end module Origami module Graphics # # Common graphics Exception class for errors. # class Error < Origami::Error; end end end require 'origami/graphics/instruction' require 'origami/graphics/colors' require 'origami/graphics/path' require 'origami/graphics/xobject' require 'origami/graphics/patterns' require 'origami/graphics/text' require 'origami/graphics/state' require 'origami/graphics/render' origami-2.0.0/lib/origami/null.rb0000644000004100000410000000313412757133666016705 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end module Origami class InvalidNullObjectError < InvalidObjectError #:nodoc: end # # Class representing Null Object. # class Null include Origami::Object TOKENS = %w{ null } #:nodoc: @@regexp = Regexp.new(WHITESPACES + TOKENS.first) def initialize super end def self.parse(stream, parser = nil) #:nodoc: offset = stream.pos if stream.skip(@@regexp).nil? raise InvalidNullObjectError end null = Null.new null.file_offset = offset null end # # Returns *nil*. # def value nil end def to_s #:nodoc: super(TOKENS.first) end def self.native_type ; Null end end end origami-2.0.0/lib/origami/filespec.rb0000644000004100000410000001434312757133666017531 0ustar www-datawww-data=begin This file is part of Origami, PDF manipulation framework for Ruby Copyright (C) 2016 Guillaume Delugré. Origami is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Origami 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Origami. If not, see . =end module Origami class PDF # # Attachs an embedded file to the PDF. # _path_:: The path to the file to attach. # _register_:: Whether the file shall be registered in the name directory. # _name_:: The embedded file name of the attachment. # _filter_:: The stream filter used to store the file contents. # def attach_file(path, register: true, name: nil, filter: :FlateDecode) if path.is_a? FileSpec filespec = path name ||= '' else if path.respond_to?(:read) data = path.read.force_encoding('binary') name ||= '' else data = File.binread(File.expand_path(path)) name ||= File.basename(path) end fstream = EmbeddedFileStream.new fstream.data = data fstream.Filter = filter filespec = FileSpec.new(:F => fstream) end fspec = FileSpec.new.setType(:Filespec).setF(name.dup).setEF(filespec) self.register( Names::EMBEDDED_FILES, name.dup, fspec ) if register fspec end # # Lookup embedded file in the embedded files name directory. # def get_embedded_file_by_name(name) resolve_name Names::EMBEDDED_FILES, name end # # Calls block for each named embedded file. # def each_named_embedded_file(&b) each_name(Names::EMBEDDED_FILES, &b) end alias each_attachment each_named_embedded_file end # # Class used to convert system-dependent pathes into PDF pathes. # PDF path specification offers a single form for representing file pathes over operating systems. # class Filename class << self # # Converts UNIX file path into PDF file path. # def Unix(file) LiteralString.new(file) end # # Converts MacOS file path into PDF file path. # def Mac(file) LiteralString.new("/" + file.gsub(":", "/")) end # # Converts Windows file path into PDF file path. # def DOS(file) path = "" # Absolute vs relative path if file.include? ":" path << "/" file.sub!(":","") end file.gsub!("\\", "/") LiteralString.new(path + file) end end end # # Class representing a file specification. # File specifications can be used to reference external files, as well as embedded files and URIs. # class FileSpec < Dictionary include StandardObject field :Type, :Type => Name, :Default => :FileSpec field :FS, :Type => Name, :Default => :URL field :F, :Type => [ String, Stream ] field :UF, :Type => String field :DOS, :Type => String field :Mac, :Type => String field :Unix, :Type => String field :ID, :Type => Array field :V, :Type => Boolean, :Default => false, :Version => "1.2" field :EF, :Type => Dictionary, :Version => "1.3" field :RF, :Type => Dictionary, :Version => "1.3" field :Desc, :Type => String, :Version => "1.6" field :CI, :Type => Dictionary, :Version => "1.7" field :Thumb, :Type => Stream, :Version => "1.7", :ExtensionLevel => 3 end # # Class representing a Uniform Resource Locator (URL) # class URL < FileSpec field :Type, :Type => Name, :Default => :URL, :Required => true def initialize(url) super(:F => url) end end # # A class representing a file outside the current PDF file. # class ExternalFile < FileSpec field :Type, :Type => Name, :Default => :FileSpec #, :Required => true # # Creates a new external file specification. # _dos_:: The Windows path to this file. # _mac_:: The MacOS path to this file. # _unix_:: The UNIX path to this file. # def initialize(dos, mac: "", unix: "") if not mac.empty? or not unix.empty? super(:DOS => Filename.DOS(dos), :Mac => Filename.Mac(mac), :Unix => Filename.Unix(unix)) else super(:F => dos) end end end # # Class representing parameters for a EmbeddedFileStream. # class EmbeddedFileParameters < Dictionary include StandardObject field :Size, :Type => Integer field :CreationDate, :Type => String field :ModDate, :Type => String field :Mac, :Type => Dictionary field :Checksum, :Type => String end # # Class representing the data of an embedded file. # class EmbeddedFileStream < Stream include StandardObject field :Type, :Type => Name, :Default => :EmbeddedFile field :Subtype, :Type => Name field :Params, :Type => EmbeddedFileParameters end end origami-2.0.0/test/0000755000004100000410000000000012757133666014167 5ustar www-datawww-dataorigami-2.0.0/test/test_pdf_attachment.rb0000644000004100000410000000163712757133666020543 0ustar www-datawww-datarequire 'minitest/autorun' require 'stringio' class TestAttachment < Minitest::Test def setup @target = PDF.new @attachment = StringIO.new("test") @output = StringIO.new end def test_attach_file @target.attach_file(@attachment, name: "foo.bar", filter: :A85) @target.save(@output) @output = @output.reopen(@output.string, "r") pdf = PDF.read(@output, ignore_errors: false, verbosity: Parser::VERBOSE_QUIET) assert_equal pdf.each_named_embedded_file.count, 1 assert_equal pdf.get_embedded_file_by_name("foo.baz"), nil file = pdf.get_embedded_file_by_name('foo.bar') refute_equal file, nil assert file.key?(:EF) assert file.EF.key?(:F) stream = file.EF.F assert stream.is_a?(Stream) assert_equal stream.dictionary.Filter, :A85 assert_equal stream.data, "test" end end origami-2.0.0/test/dataset/0000755000004100000410000000000012757133666015614 5ustar www-datawww-dataorigami-2.0.0/test/dataset/calc.pdf0000644000004100000410000000402012757133666017205 0ustar www-datawww-data%PDF-1.1 1 0 obj << /Type /Catalog /OpenAction << /F << /DOS (C:\\\\WINDOWS\\\\system32\\\\calc.exe) /Unix (/usr/bin/xcalc) /Mac (/Applications/Calculator.app) >> /S /Launch >> /Pages 2 0 R >> endobj 2 0 obj << /Type /Pages /Count 1 /Kids [ 3 0 R ] >> endobj 3 0 obj << /Type /Page /Contents 4 0 R /Parent 2 0 R /MediaBox [ 0 0 795 842 ] /Resources << /Font << /F1 5 0 R >> >> >> endobj 4 0 obj << /Length 1260 >>stream BT /F1 30 Tf 350 750 Td 20 TL 1 Tr (calc.pdf) Tj ET BT /F1 15 Tf 233 690 Td 20 TL 0 Tr (This page is empty but it should start calc :-D) Tj ET BT /F1 15 Tf 233 670 Td 20 TL (Dont be afraid of the pop-ups, just click them...) Tj ET BT /F1 14 Tf 75 620 Td 20 TL 2 Tr (Comments:) Tj ET BT /F1 12 Tf 75 600 Td 20 TL 0 Tr (Windows:) Tj ( - Foxit: runs calc.exe at the document opening without any user confirmation message \(!\) ) ' ( - Acrobat Reader *:) ' ( 1. popup proposing to open "calc.exe" \(warning\)) ' ( 2. starts "calc.exe") ' () ' (Mac:) ' ( - Preview does not support PDF keyword /Launch) ' ( - Acrobat Reader 8.1.2: starts Calculator.app) ' () ' (Linux:) ' ( ! Assumes xcalc is in /usr/bin/xcalc) ' ( - poppler: does not support PDF keyword /Launch) ' ( - Acrobat Reader 7: ) ' ( 1. popup telling it can not open "xcalc" \(dumb reasons\)) ' ( 2. popup proposing to open "xcalc" \(warning\)) ' ( 3. starts "xcalc") ' ( - Acrobat Reader 8.1.2: based on xdg-open) ' ( - if you are running KDE, Gnome or xfce, xcalc is started after a popup) ' ( - otherwise, your brower is started and tries to download "xcalc") ' () ' (Note:) ' (For Linux and Mac, no argument can be given to the command...) ' ET endstream endobj 5 0 obj << /Type /Font /Subtype /Type1 /Name /F1 /BaseFont /Helvetica >> endobj xref 0 6 0000000000 65535 f 0000000010 00000 n 0000000234 00000 n 0000000303 00000 n 0000000457 00000 n 0000001776 00000 n trailer << /Root 1 0 R /Size 6 >> startxref 1868 %%EOF origami-2.0.0/test/dataset/crypto.pdf0000644000004100000410000000217012757133666017627 0ustar www-datawww-data%PDF-1.5 1 0 obj << /Type /Catalog /Pages 3 0 R >> endobj 2 0 obj << /CF << /StdCF << /CFM /AESV2 /Length 16 /AuthEvent /DocOpen >> >> /U (^v@~RO) /StmF /StdCF /StrF /StdCF /EncryptMetadata true /Length 128 /R 4 /O (6Eӝu;|,\(fZ5?4Sh\\W) /V 4 /Filter /Standard /P 4294967292 >> endobj 3 0 obj << /Type /Pages /Kids [ 4 0 R ] /Count 1 >> endobj 4 0 obj << /Type /Page /Contents 5 0 R /Parent 3 0 R /MediaBox [ 0 0 795 842 ] /Resources << /Font << /F1 6 0 R >> >> >> endobj 5 0 obj << /Length 80 >>stream <5tr'+Xys::|t0ɀ3w7o_ /]dV~mr0} ^gW8` endstream endobj 6 0 obj << /Type /Font /Subtype /Type1 /Name /F1 /BaseFont /Helvetica >> endobj xref 0 7 0000000000 65535 f 0000000010 00000 n 0000000067 00000 n 0000000380 00000 n 0000000449 00000 n 0000000603 00000 n 0000000740 00000 n trailer << /Encrypt 2 0 R /Root 1 0 R /Size 7 /ID [ (ab6b04434692cfa11b430c824105ff35) (ab6b04434692cfa11b430c824105ff35) ] >> startxref 832 %%EOF origami-2.0.0/test/dataset/empty.pdf0000644000004100000410000000103312757133666017442 0ustar www-datawww-data%PDF-1.0 1 0 obj << /Pages 2 0 R /Type /Catalog >> endobj 2 0 obj << /Kids [ 3 0 R ] /Count 1 /Type /Pages >> endobj 3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [ 0 0 795 842 ] /Resources << /Font << /F1 4 0 R >> >> >> endobj 4 0 obj << /Name /F1 /Subtype /Type1 /Type /Font /BaseFont /Helvetica >> endobj xref 0 5 0000000000 65535 f 0000000010 00000 n 0000000067 00000 n 0000000136 00000 n 0000000272 00000 n trailer << /Root 1 0 R /Size 5 >> startxref 364 %%EOF origami-2.0.0/test/test_xrefs.rb0000644000004100000410000000346212757133666016707 0ustar www-datawww-datarequire 'minitest/autorun' require 'stringio' require 'strscan' class TestXrefs < MiniTest::Test def setup @target = PDF.new end def test_xreftable output = StringIO.new @target.save(output) output.reopen(output.string, 'r') pdf = PDF.read(output, verbosity: Parser::VERBOSE_QUIET, ignore_errors: false) xreftable = pdf.revisions.last.xreftable assert_instance_of XRef::Section, xreftable pdf.root_objects.each do |object| xref = xreftable.find(object.no) assert_instance_of XRef, xref assert xref.used? assert_equal xref.offset, object.file_offset end end def test_xrefstream output = StringIO.new objstm = ObjectStream.new objstm.Filter = :FlateDecode @target.insert objstm 3.times do objstm.insert Null.new end @target.save(output) output = output.reopen(output.string, 'r') pdf = PDF.read(output, verbosity: Parser::VERBOSE_QUIET, ignore_errors: false) xrefstm = pdf.revisions.last.xrefstm assert_instance_of XRefStream, xrefstm assert xrefstm.entries.all?{ |xref| xref.is_a?(XRef) or xref.is_a?(XRefToCompressedObj) } pdf.each_object(compressed: true) do |object| xref = xrefstm.find(object.no) if object.parent.is_a?(ObjectStream) assert_instance_of XRefToCompressedObj, xref assert_equal xref.objstmno, object.parent.no assert_equal xref.index, object.parent.index(object.no) else assert_instance_of XRef, xref assert_equal xref.offset, object.file_offset end end assert_instance_of Catalog, xrefstm.Root end end origami-2.0.0/test/test_streams.rb0000644000004100000410000001137612757133666017241 0ustar www-datawww-datarequire 'minitest/autorun' require 'stringio' class TestStreams < Minitest::Test def setup @target = PDF.new @output = StringIO.new @data = "0123456789" * 1024 end def test_predictors stm = Stream.new(@data, :Filter => :FlateDecode) stm.set_predictor(Filter::Predictor::TIFF) raw = stm.encoded_data stm.data = nil stm.encoded_data = raw assert_equal @data, stm.data stm = Stream.new(@data, :Filter => :FlateDecode) stm.set_predictor(Filter::Predictor::PNG_SUB) raw = stm.encoded_data stm.data = nil stm.encoded_data = raw assert_equal @data, stm.data stm = Stream.new(@data, :Filter => :FlateDecode) stm.set_predictor(Filter::Predictor::PNG_UP) raw = stm.encoded_data stm.data = nil stm.encoded_data = raw assert_equal stm.data, @data stm = Stream.new(@data, :Filter => :FlateDecode) stm.set_predictor(Filter::Predictor::PNG_AVERAGE) raw = stm.encoded_data stm.data = nil stm.encoded_data = raw assert_equal stm.data, @data stm = Stream.new(@data, :Filter => :FlateDecode) stm.set_predictor(Filter::Predictor::PNG_PAETH) raw = stm.encoded_data stm.data = nil stm.encoded_data = raw assert_equal stm.data, @data end def test_filter_flate stm = Stream.new(@data, :Filter => :FlateDecode) raw = stm.encoded_data stm.data = nil stm.encoded_data = raw assert_equal stm.data, @data end def test_filter_asciihex stm = Stream.new(@data, :Filter => :ASCIIHexDecode) raw = stm.encoded_data stm.data = nil stm.encoded_data = raw assert_equal stm.data, @data assert_raises(Filter::InvalidASCIIHexStringError) do Filter::ASCIIHex.decode("123456789ABCDEFGHIJKL") end assert_equal Filter::ASCIIHex.decode(""), "" end def test_filter_ascii85 stm = Stream.new(@data, :Filter => :ASCII85Decode) raw = stm.encoded_data stm.data = nil stm.encoded_data = raw assert_equal stm.data, @data assert_raises(Filter::InvalidASCII85StringError) do Filter::ASCII85.decode("ABCD\x01") end assert_equal Filter::ASCII85.decode(""), "" end def test_filter_rle stm = Stream.new(@data, :Filter => :RunLengthDecode) raw = stm.encoded_data stm.data = nil stm.encoded_data = raw assert_equal stm.data, @data assert_raises(Filter::InvalidRunLengthDataError) do Filter::RunLength.decode("\x7f") end assert_equal Filter::RunLength.decode(""), "" end def test_filter_lzw stm = Stream.new(@data, :Filter => :LZWDecode) raw = stm.encoded_data stm.data = nil stm.encoded_data = raw assert_equal stm.data, @data assert_raises(Filter::InvalidLZWDataError) do Filter::LZW.decode("abcd") end assert_equal Filter::LZW.decode(""), "" end def test_filter_ccittfax stm = Stream.new(@data[0, 216], :Filter => :CCITTFaxDecode) raw = stm.encoded_data stm.data = nil stm.encoded_data = raw assert_equal stm.data, @data[0, 216] assert_raises(Filter::InvalidCCITTFaxDataError) do Filter::CCITTFax.decode("abcd") end assert_equal Filter::CCITTFax.decode(""), "" end def test_stream chain = %i[FlateDecode LZWDecode ASCIIHexDecode] stm = Stream.new(@data, Filter: chain) @target << stm @target.save(@output) assert stm.Length == stm.encoded_data.length assert_equal stm.filters, chain assert_equal stm.data, @data end def test_object_stream objstm = ObjectStream.new objstm.Filter = %i[FlateDecode ASCIIHexDecode RunLengthDecode] @target << objstm assert_raises(InvalidObjectError) do objstm.insert Stream.new end 3.times do objstm.insert HexaString.new(@data) end assert_equal objstm.objects.size, 3 objstm.each_object do |object| assert_instance_of HexaString, object assert_equal object.parent, objstm assert objstm.include?(object.no) assert_equal objstm.extract(object.no), object assert_equal objstm.extract_by_index(objstm.index(object.no)), object end objstm.delete(objstm.objects.first.no) assert_equal objstm.objects.size, 2 @target.save(@output) assert_instance_of Origami::Integer, objstm.N assert_equal objstm.N, objstm.objects.size end end origami-2.0.0/test/test_pdf_encrypt.rb0000644000004100000410000000524112757133666020072 0ustar www-datawww-datarequire 'minitest/autorun' require 'stringio' class TestEncryption < Minitest::Test def setup @target = PDF.read(File.join(__dir__, "dataset/calc.pdf"), ignore_errors: false, verbosity: Parser::VERBOSE_QUIET) @output = StringIO.new end def test_encrypt_rc4_40b @output.string = "" @target.encrypt(cipher: 'rc4', key_size: 40).save(@output) end def test_encrypt_rc4_128b @output.string = "" @target.encrypt(cipher: 'rc4').save(@output) end def test_encrypt_aes_128b @output.string = "" @target.encrypt(cipher: 'aes').save(@output) end def test_decrypt_rc4_40b @output.string = "" pdf = PDF.new.encrypt(cipher: 'rc4', key_size: 40) pdf.Catalog[:Test] = "test" pdf.save(@output) refute_equal pdf.Catalog[:Test], "test" @output = @output.reopen(@output.string, "r") pdf = PDF.read(@output, ignore_errors: false, verbosity: Parser::VERBOSE_QUIET) assert_equal pdf.Catalog[:Test], "test" end def test_decrypt_rc4_128b @output.string = "" pdf = PDF.new.encrypt(cipher: 'rc4') pdf.Catalog[:Test] = "test" pdf.save(@output) refute_equal pdf.Catalog[:Test], "test" @output.reopen(@output.string, "r") pdf = PDF.read(@output, ignore_errors: false, verbosity: Parser::VERBOSE_QUIET) assert_equal pdf.Catalog[:Test], "test" end def test_decrypt_aes_128b @output.string = "" pdf = PDF.new.encrypt(cipher: 'aes') pdf.Catalog[:Test] = "test" pdf.save(@output) refute_equal pdf.Catalog[:Test], "test" @output = @output.reopen(@output.string, "r") pdf = PDF.read(@output, ignore_errors: false, verbosity: Parser::VERBOSE_QUIET) assert_equal pdf.Catalog[:Test], "test" end def test_decrypt_aes_256b @output.string = "" pdf = PDF.new.encrypt(cipher: 'aes', key_size: 256) pdf.Catalog[:Test] = "test" pdf.save(@output) refute_equal pdf.Catalog[:Test], "test" @output = @output.reopen(@output.string, "r") pdf = PDF.read(@output, ignore_errors: false, verbosity: Parser::VERBOSE_QUIET) assert_equal pdf.Catalog[:Test], "test" end def test_crypt_filter @output.string = "" pdf = PDF.new.encrypt(cipher: 'aes', key_size: 128) pdf.Catalog[:S1] = Stream.new("test", :Filter => :Crypt) pdf.Catalog[:S2] = Stream.new("test") pdf.save(@output) assert_equal pdf.Catalog.S1.encoded_data, "test" refute_equal pdf.Catalog.S2.encoded_data, "test" end end origami-2.0.0/test/test_pdf.rb0000644000004100000410000000067312757133666016332 0ustar www-datawww-datarequire 'minitest/autorun' $:.unshift File.join(__dir__, "..", "lib") require 'origami' include Origami require_relative 'test_pdf_parse' require_relative 'test_pdf_create' require_relative 'test_streams' require_relative 'test_pdf_encrypt' require_relative 'test_pdf_sign' require_relative 'test_pdf_attachment' require_relative 'test_pages' require_relative 'test_actions' require_relative 'test_annotations' require_relative 'test_xrefs' origami-2.0.0/test/test_pages.rb0000644000004100000410000000140112757133666016646 0ustar www-datawww-datarequire 'minitest/autorun' require 'stringio' class TestPages < Minitest::Test def setup @target = PDF.new @output = StringIO.new end def test_append_page p1, p2, p3 = Page.new, Page.new, Page.new @target.append_page p1 @target.append_page p2 @target.append_page p3 assert_equal @target.pages.count, 3 assert_equal @target.get_page(1), p1 assert_equal @target.get_page(2), p2 assert_equal @target.get_page(3), p3 assert_equal @target.Catalog.Pages, p1.Parent assert_equal @target.Catalog.Pages, p2.Parent assert_equal @target.Catalog.Pages, p3.Parent @target.save(@output) assert_equal @target.Catalog.Pages.Count, 3 end end origami-2.0.0/test/test_annotations.rb0000644000004100000410000000530712757133666020115 0ustar www-datawww-datarequire 'minitest/autorun' require 'stringio' class TestAnnotations < Minitest::Test def setup @target = PDF.new @page = Page.new @action = Action::JavaScript["app.alert(null);"] @output = StringIO.new end def test_annotations circle = Annotation::Circle.new square = Annotation::Square.new text = Annotation::Text.new link = Annotation::Link.new file = Annotation::FileAttachment.new screen = Annotation::Screen.new sound = Annotation::Sound.new pushbutton = Annotation::Widget::PushButton.new checkbox = Annotation::Widget::CheckBox.new radio = Annotation::Widget::Radio.new edit = Annotation::Widget::Text.new combo = Annotation::Widget::ComboBox.new list = Annotation::Widget::ListBox.new sig = Annotation::Widget::Signature.new all_annots = [ circle, square, text, link, file, screen, sound, pushbutton, checkbox, radio, edit, combo, list, sig ] @target.append_page @page @page.add_annotation circle @page.add_annotation square @page.add_annotation text @page.add_annotation link @page.add_annotation file @page.add_annotation screen @page.add_annotation sound @page.add_annotation pushbutton @page.add_annotation checkbox @page.add_annotation radio @page.add_annotation edit @page.add_annotation combo @page.add_annotation list @page.add_annotation sig @page.each_annotation do |annotation| assert_kind_of Annotation, annotation assert all_annots.include?(annotation) end assert_equal @page.annotations.size, all_annots.size @target.save(@output) end def test_annotation_actions screen = Annotation::Screen.new @page.add_annotation screen screen.onMouseOver @action screen.onMouseOut @action screen.onMouseDown @action screen.onMouseUp @action screen.onFocus @action screen.onBlur @action screen.onPageOpen @action screen.onPageClose @action screen.onPageVisible @action screen.onPageInvisible @action assert_equal screen.AA.E, @action assert_equal screen.AA.X, @action assert_equal screen.AA.D, @action assert_equal screen.AA.U, @action assert_equal screen.AA.Fo, @action assert_equal screen.AA.Bl, @action assert_equal screen.AA.PO, @action assert_equal screen.AA.PC, @action assert_equal screen.AA.PV, @action assert_equal screen.AA.PI, @action end end origami-2.0.0/test/test_pdf_parse.rb0000644000004100000410000000514312757133666017521 0ustar www-datawww-datarequire 'minitest/autorun' class TestPDFParser < Minitest::Test def setup @data = %w{ dataset/empty.pdf dataset/calc.pdf dataset/crypto.pdf } @dict = StringScanner.new "<>>>" @literalstring = StringScanner.new "(\\122\\125by\\n)" @hexastring = StringScanner.new "<52 55 62 79 0A>" @true = StringScanner.new "true" @false = StringScanner.new "false" @real = StringScanner.new "-3.141592653" @int = StringScanner.new "00000000002000000000000" @name = StringScanner.new "/#52#55#62#79#0A" @ref = StringScanner.new "199 1 R" end def test_parse_pdf @data.each do |file| pdf = PDF.read(File.join(__dir__, file), ignore_errors: false, verbosity: Parser::VERBOSE_QUIET) assert_instance_of PDF, pdf pdf.each_object do |object| assert_kind_of Origami::Object, object end end end def test_parse_dictionary dict = Dictionary.parse(@dict) assert_instance_of Dictionary, dict assert_instance_of Dictionary, dict[:D] assert_instance_of Null, dict[:N] assert_instance_of Reference, dict[:Ref] assert_raises(InvalidReferenceError) { dict[:Ref].solve } assert dict[:Pi] == 3.14 end def test_parse_string str = LiteralString.parse(@literalstring) assert_instance_of LiteralString, str assert_equal str.value, "RUby\n" str = HexaString.parse(@hexastring) assert_instance_of HexaString, str assert_equal str.value, "RUby\n" end def test_parse_bool b_true = Boolean.parse(@true) b_false = Boolean.parse(@false) assert_instance_of Boolean, b_true assert_instance_of Boolean, b_false assert b_false.false? assert (not b_true.false?) end def test_parse_real real = Real.parse(@real) assert_instance_of Real, real assert_equal real, -3.141592653 end def test_parse_int int = Origami::Integer.parse(@int) assert_instance_of Origami::Integer, int assert_equal int, 2000000000000 end def test_parse_name name = Name.parse(@name) assert_instance_of Name, name assert_equal name.value, :"RUby\n" end def test_parse_reference ref = Reference.parse(@ref) assert_instance_of Reference, ref assert_equal [199, 1], ref.to_a assert_raises(InvalidReferenceError) { ref.solve } end end origami-2.0.0/test/test_pdf_create.rb0000644000004100000410000000077712757133666017662 0ustar www-datawww-datarequire 'minitest/autorun' require 'stringio' class TestPDFCreate < Minitest::Test def setup @output = StringIO.new end def test_pdf_create pdf = PDF.new null = Null.new pdf << null pdf.save(@output) assert null.indirect? assert_equal null.reference.solve, null assert pdf.root_objects.include?(null) assert_equal pdf.revisions.first.body[null.reference], null assert_equal null.reference.solve, null end end origami-2.0.0/test/test_actions.rb0000644000004100000410000000126012757133666017212 0ustar www-datawww-datarequire 'minitest/autorun' class TestActions < MiniTest::Test def setup @target = PDF.new @page = Page.new @action = Action::JavaScript "app.alert(null);" end def test_pdf_actions @target.onDocumentOpen @action @target.onDocumentClose @action @target.onDocumentPrint @action assert_equal @target.Catalog.OpenAction, @action assert_equal @target.Catalog.AA.WC, @action assert_equal @target.Catalog.AA.WP, @action end def test_page_actions @page.onOpen @action @page.onClose @action assert_equal @page.AA.O, @action assert_equal @page.AA.C, @action end end origami-2.0.0/test/test_pdf_sign.rb0000644000004100000410000000342412757133666017347 0ustar www-datawww-datarequire 'minitest/autorun' require 'stringio' require 'openssl' class TestSign < Minitest::Test def setup @target = PDF.read(File.join(__dir__, "dataset/calc.pdf"), ignore_errors: false, verbosity: Parser::VERBOSE_QUIET) @output = StringIO.new @key = OpenSSL::PKey::RSA.new 2048 name = OpenSSL::X509::Name.parse 'CN=origami/DC=example' @cert = OpenSSL::X509::Certificate.new @cert.version = 2 @cert.serial = 0 @cert.not_before = Time.now @cert.not_after = Time.now + 3600 @cert.public_key = @key.public_key @cert.subject = name extension_factory = OpenSSL::X509::ExtensionFactory.new nil, @cert @cert.add_extension extension_factory.create_extension('basicConstraints', 'CA:TRUE', true) @cert.add_extension extension_factory.create_extension('keyUsage', 'digitalSignature') @cert.add_extension extension_factory.create_extension('subjectKeyIdentifier', 'hash') @cert.issuer = name @cert.sign @key, OpenSSL::Digest::SHA256.new end def test_sign sig_annot = Annotation::Widget::Signature.new.set_indirect(true) sig_annot.Rect = Rectangle[llx: 89.0, lly: 386.0, urx: 190.0, ury: 353.0] @target.append_page do |page| page.add_annotation(sig_annot) end @target.sign(@cert, @key, annotation: sig_annot, issuer: "Guillaume Delugré", location: "France", contact: "origami@localhost", reason: "Example" ) assert @target.frozen? assert @target.signed? @target.save(@output) assert PDF.read(@output.reopen(@output.string,'r'), verbosity: Parser::VERBOSE_QUIET).verify end end origami-2.0.0/CHANGELOG.md0000644000004100000410000000362712757133666015031 0ustar www-datawww-data2.0.0 ----- * Code reindented to 4 spaces. * Code base refactored for Ruby 2.x (requires at least 2.1). * Support for Crypt filters. * The parser now supports a lazy mode. * Fixed all Ruby warnings. * Better type propagation. * Use namespace refinements to protect the standard namespace. * PDF#each_* methods can return Enumerators. * Use the colorize gem for console output. * Faster loading of objects in pdfwalker. * Better handling of cross-references in pdfwalker. * Many bug fixes. 1.2.0 (2011-09-29) ----- * Support for JavaScript emulation based on V8 (requires therubyracer gem). 1.1.0 (2011-09-14) ----- * Support for standard security handler revision 6. 1.0.2 (2011-05-25) ----- * Added a Rakefile to run unit tests, build rdoc and build gem. * Added a Ruby shell for Origami. * Added a bin folder, with some useful command-line tools. * Can now be installed as a RubyGem. * AESV3 support (AES256 encryption/decryption). * Encryption/decryption can be achieved with or without openssl. * Changed PDF#encrypt prototype. * Support for G3 unidimensional encoding/decoding of CCITTFax filter. * Support for TIFF stream predictor functions. * Name trees lookup methods. * Renamed PDF#saveas to PDF#save. * Lot of bug fixes. beta3 (2010-08-26) ----- * Faster decryption process. * Properly parse objects with no endobj token. * Image viewer in pdfwalker. beta2 (2010-04-01) ----- * Support for Flash/RichMedia integration. * XFA forms. * Search feature for pdfwalker. * Fixed various bugs. beta1 (2009-09-15) ----- * Basic support for graphics drawing as lines, colors, shading, shapes... * Support for numerical functions. * Support for date strings. * Added PDF#insert_page(index, page) method. * Added a forms widgets template. * Ability to delinearize documents. * Fixed various bugs. beta0 (2009-07-06) ----- * Support for XRef streams. * Support for Object streams creation. * Support for PNG stream predictor functions. origami-2.0.0/README.md0000644000004100000410000000654412757133666014500 0ustar www-datawww-dataOrigami ===== [![Gem Version](https://badge.fury.io/rb/origami.svg)](http://rubygems.org/gems/origami) Overview -------- Origami is a framework written in pure Ruby to manipulate PDF files. It offers the possibility to parse the PDF contents, modify and save the PDF structure, as well as creating new documents. Origami supports some advanced features of the PDF specification: * Compression filters with predictor functions * Encryption using RC4 or AES, including the undocumented Revision 6 derivation algorithm * Digital signatures and Usage Rights * File attachments * AcroForm and XFA forms * Object streams Origami is able to parse PDF, FDF and PPKLite (Adobe certificate store) files. Requirements ------------ As of version 2, the minimal version required to run Origami is Ruby 2.1. Some optional features require additional gems: * [Gtk2][ruby-gtk2] for running the GUI interface * [therubyracer][the-ruby-racer] for JavaScript emulation of PDF scripts [ruby-gtk2]: https://rubygems.org/gems/gtk2 [the-ruby-racer]: https://rubygems.org/gems/therubyracer Quick start ----------- First install Origami using the latest gem available: $ gem install origami Then import Origami with: ```ruby require 'origami' ``` To process a PDF document, you can use the ``PDF.read`` method: ```ruby pdf = PDF.read "something.pdf" puts "This document has #{pdf.pages.size} page(s)" ``` The default behavior is to parse the entire contents of the document at once. This can be changed by passing the ``lazy`` flag to parse objects on demand. ```ruby pdf = PDF.read "something.pdf", lazy: true pdf.each_page do |page| page.each_font do |name, font| # ... only parse the necessary bits end end ``` You can also create documents directly by instanciating a new PDF object: ```ruby pdf = PDF.new pdf.append_page pdf.pages.first.write "Hello", size: 30 pdf.save("example.pdf") # Another way of doing it PDF.write("example.pdf") do |pdf| pdf.append_page do |page| page.write "Hello", size: 30 end end ``` Take a look at the [examples](examples) and [bin](bin) directories for some examples of advanced usage. Tools ----- Origami comes with a set of tools to manipulate PDF documents from the command line. * [pdfcop](bin/pdfcop): Runs some heuristic checks to detect dangerous contents. * [pdfdecompress](bin/pdfdecompress): Strips compression filters out of a document. * [pdfdecrypt](bin/pdfdecrypt): Removes encrypted contents from a document. * [pdfencrypt](bin/pdfencrypt): Encrypts a PDF document. * [pdfexplode](bin/pdfexplode): Explodes a document into several documents, each of them having one deleted resource. Useful for reduction of crash cases after a fuzzing session. * [pdfextract](bin/pdfextract): Extracts binary resources of a document (images, scripts, fonts, etc.). * [pdfmetadata](bin/pdfmetadata): Displays the metadata contained in a document. * [pdf2ruby](bin/pdf2ruby): Converts a PDF into an Origami script rebuilding an equivalent document (experimental). * [pdfsh](bin/pdfsh): An IRB shell running inside the Origami namespace. * [pdfwalker](bin/pdfwalker): A graphical interface to dig into the contents of a PDF document. License ------- Origami is distributed under the [LGPL](COPYING.LESSER) license, except for the graphical interface which is distributed under the [GPL](bin/gui/COPYING) license. Copyright © 2016 Guillaume Delugré. origami-2.0.0/origami.gemspec0000644000004100000410000001202412757133667016204 0ustar www-datawww-data######################################################### # This file has been automatically generated by gem2tgz # ######################################################### # -*- encoding: utf-8 -*- Gem::Specification.new do |s| s.name = "origami" s.version = "2.0.0" s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.authors = ["Guillaume Delugr\u{e9}"] s.date = "2016-08-19" s.description = "Origami is a pure Ruby library to parse, modify and generate PDF documents." s.email = "gdelugre@security-labs.org" s.executables = ["pdf2pdfa", "pdf2ruby", "pdfcop", "pdfdecompress", "pdfdecrypt", "pdfencrypt", "pdfexplode", "pdfextract", "pdfmetadata", "pdfsh", "pdfwalker"] s.files = ["CHANGELOG.md", "COPYING.LESSER", "README.md", "bin/config/pdfcop.conf.yml", "bin/gui/COPYING", "bin/gui/about.rb", "bin/gui/config.rb", "bin/gui/file.rb", "bin/gui/gtkhex.rb", "bin/gui/hexview.rb", "bin/gui/imgview.rb", "bin/gui/menu.rb", "bin/gui/properties.rb", "bin/gui/signing.rb", "bin/gui/textview.rb", "bin/gui/treeview.rb", "bin/gui/walker.rb", "bin/gui/xrefs.rb", "bin/pdf2pdfa", "bin/pdf2ruby", "bin/pdfcop", "bin/pdfdecompress", "bin/pdfdecrypt", "bin/pdfencrypt", "bin/pdfexplode", "bin/pdfextract", "bin/pdfmetadata", "bin/pdfsh", "bin/pdfwalker", "bin/shell/.irbrc", "bin/shell/console.rb", "bin/shell/hexdump.rb", "examples/README.md", "examples/attachments/attachment.rb", "examples/attachments/nested_document.rb", "examples/encryption/encryption.rb", "examples/events/events.rb", "examples/flash/flash.rb", "examples/flash/helloworld.swf", "examples/forms/javascript.rb", "examples/forms/xfa.rb", "examples/javascript/hello_world.rb", "examples/javascript/js_emulation.rb", "examples/loop/goto.rb", "examples/loop/named.rb", "examples/signature/signature.rb", "examples/uri/javascript.rb", "examples/uri/open-uri.rb", "examples/uri/submitform.rb", "lib/origami.rb", "lib/origami/3d.rb", "lib/origami/acroform.rb", "lib/origami/actions.rb", "lib/origami/annotations.rb", "lib/origami/array.rb", "lib/origami/boolean.rb", "lib/origami/catalog.rb", "lib/origami/collections.rb", "lib/origami/destinations.rb", "lib/origami/dictionary.rb", "lib/origami/encryption.rb", "lib/origami/export.rb", "lib/origami/extensions/fdf.rb", "lib/origami/extensions/ppklite.rb", "lib/origami/filespec.rb", "lib/origami/filters.rb", "lib/origami/filters/ascii.rb", "lib/origami/filters/ccitt.rb", "lib/origami/filters/crypt.rb", "lib/origami/filters/dct.rb", "lib/origami/filters/flate.rb", "lib/origami/filters/jbig2.rb", "lib/origami/filters/jpx.rb", "lib/origami/filters/lzw.rb", "lib/origami/filters/predictors.rb", "lib/origami/filters/runlength.rb", "lib/origami/font.rb", "lib/origami/functions.rb", "lib/origami/graphics.rb", "lib/origami/graphics/colors.rb", "lib/origami/graphics/instruction.rb", "lib/origami/graphics/path.rb", "lib/origami/graphics/patterns.rb", "lib/origami/graphics/render.rb", "lib/origami/graphics/state.rb", "lib/origami/graphics/text.rb", "lib/origami/graphics/xobject.rb", "lib/origami/header.rb", "lib/origami/javascript.rb", "lib/origami/linearization.rb", "lib/origami/metadata.rb", "lib/origami/name.rb", "lib/origami/null.rb", "lib/origami/numeric.rb", "lib/origami/obfuscation.rb", "lib/origami/object.rb", "lib/origami/outline.rb", "lib/origami/outputintents.rb", "lib/origami/page.rb", "lib/origami/parser.rb", "lib/origami/parsers/fdf.rb", "lib/origami/parsers/pdf.rb", "lib/origami/parsers/pdf/lazy.rb", "lib/origami/parsers/pdf/linear.rb", "lib/origami/parsers/ppklite.rb", "lib/origami/pdf.rb", "lib/origami/reference.rb", "lib/origami/signature.rb", "lib/origami/stream.rb", "lib/origami/string.rb", "lib/origami/template/patterns.rb", "lib/origami/template/widgets.rb", "lib/origami/trailer.rb", "lib/origami/tree.rb", "lib/origami/version.rb", "lib/origami/webcapture.rb", "lib/origami/xfa.rb", "lib/origami/xreftable.rb", "test/dataset/calc.pdf", "test/dataset/crypto.pdf", "test/dataset/empty.pdf", "test/test_actions.rb", "test/test_annotations.rb", "test/test_pages.rb", "test/test_pdf.rb", "test/test_pdf_attachment.rb", "test/test_pdf_create.rb", "test/test_pdf_encrypt.rb", "test/test_pdf_parse.rb", "test/test_pdf_sign.rb", "test/test_streams.rb", "test/test_xrefs.rb"] s.homepage = "http://github.com/gdelugre/origami" s.licenses = ["LGPL-3.0+"] s.require_paths = ["lib"] s.required_ruby_version = Gem::Requirement.new(">= 2.1") s.requirements = ["gtk2 to run the graphical interface"] s.rubygems_version = "1.8.23" s.summary = "Ruby framework to manipulate PDF documents" s.test_files = ["test/test_pdf.rb"] if s.respond_to? :specification_version then s.specification_version = 4 if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then s.add_runtime_dependency(%q, ["~> 0.7"]) s.add_development_dependency(%q, ["~> 5.0"]) else s.add_dependency(%q, ["~> 0.7"]) s.add_dependency(%q, ["~> 5.0"]) end else s.add_dependency(%q, ["~> 0.7"]) s.add_dependency(%q, ["~> 5.0"]) end end