silk_layout 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,14 +8,20 @@ module SilkLayout
8
8
  CSS_DPI = 96.0
9
9
  PDF_DPI = 72.0
10
10
 
11
- PAGE_WIDTH_PX = 800
12
- PAGE_HEIGHT_PX = 1000
11
+ DEFAULT_PAGE_WIDTH = 800
12
+ DEFAULT_PAGE_HEIGHT = 1000
13
13
 
14
- def self.render(box_tree, output_path)
14
+ def self.render(box_tree, output_path, page_size: nil, page_width: nil, page_height: nil)
15
15
  doc = HexaPDF::Document.new
16
16
 
17
- page_width_pt = px_to_pt(PAGE_WIDTH_PX)
18
- page_height_pt = px_to_pt(PAGE_HEIGHT_PX)
17
+ resolved_page_width, resolved_page_height = page_dimensions(
18
+ page_size: page_size,
19
+ page_width: page_width,
20
+ page_height: page_height
21
+ )
22
+
23
+ page_width_pt = px_to_pt(resolved_page_width)
24
+ page_height_pt = px_to_pt(resolved_page_height)
19
25
 
20
26
  page = doc.pages.add([0, 0, page_width_pt, page_height_pt])
21
27
  Painter.paint(page.canvas, box_tree, page_height_pt)
@@ -26,6 +32,34 @@ module SilkLayout
26
32
  def self.px_to_pt(px)
27
33
  px * PDF_DPI / CSS_DPI
28
34
  end
35
+
36
+ def self.page_dimensions(page_size:, page_width:, page_height:)
37
+ width, height = page_size ? normalize_page_size(page_size) : [DEFAULT_PAGE_WIDTH, DEFAULT_PAGE_HEIGHT]
38
+
39
+ [
40
+ validate_dimension(page_width || width || DEFAULT_PAGE_WIDTH, "page_width"),
41
+ validate_dimension(page_height || height || DEFAULT_PAGE_HEIGHT, "page_height")
42
+ ]
43
+ end
44
+
45
+ def self.normalize_page_size(page_size)
46
+ case page_size
47
+ when Array
48
+ raise ArgumentError, "page_size must contain width and height" unless page_size.length == 2
49
+
50
+ page_size
51
+ when Hash
52
+ [page_size[:width] || page_size["width"], page_size[:height] || page_size["height"]]
53
+ else
54
+ raise ArgumentError, "page_size must be an Array or Hash"
55
+ end
56
+ end
57
+
58
+ def self.validate_dimension(value, name)
59
+ return value if value.is_a?(Numeric) && value.positive?
60
+
61
+ raise ArgumentError, "#{name} must be a positive number of CSS pixels"
62
+ end
29
63
  end
30
64
  end
31
65
  end
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module SilkLayout
6
+ module Resource
7
+ class Image
8
+ PNG_SIGNATURE = "\x89PNG\r\n\x1A\n".b
9
+ JPEG_START_OF_FRAME_MARKERS = [
10
+ 0xC0,
11
+ 0xC1,
12
+ 0xC2,
13
+ 0xC3,
14
+ 0xC5,
15
+ 0xC6,
16
+ 0xC7,
17
+ 0xC9,
18
+ 0xCA,
19
+ 0xCB,
20
+ 0xCD,
21
+ 0xCE,
22
+ 0xCF
23
+ ].freeze
24
+
25
+ attr_reader :uri, :path, :width, :height
26
+
27
+ def self.load(source)
28
+ uri = normalize_uri(source)
29
+ return nil unless uri
30
+ return nil unless uri.scheme.nil? || uri.scheme == "file"
31
+
32
+ path = (uri.scheme == "file") ? uri_unescape(uri.path) : uri.to_s
33
+ return nil unless File.file?(path)
34
+
35
+ dimensions = read_dimensions(path)
36
+ return nil unless dimensions
37
+
38
+ new(uri: uri, path: path, width: dimensions[0], height: dimensions[1])
39
+ rescue URI::InvalidURIError, SystemCallError
40
+ nil
41
+ end
42
+
43
+ def initialize(uri:, path:, width:, height:)
44
+ @uri = uri
45
+ @path = path
46
+ @width = width
47
+ @height = height
48
+ end
49
+
50
+ def aspect_ratio
51
+ return nil unless width&.positive? && height&.positive?
52
+
53
+ width.to_f / height
54
+ end
55
+
56
+ def self.normalize_uri(source)
57
+ return source if source.is_a?(URI)
58
+
59
+ raw = source.to_s.strip
60
+ return nil if raw.empty?
61
+
62
+ URI.parse(raw)
63
+ end
64
+
65
+ def self.read_dimensions(path)
66
+ png_dimensions(path) || jpeg_dimensions(path)
67
+ end
68
+
69
+ def self.png_dimensions(path)
70
+ File.open(path, "rb") do |file|
71
+ header = file.read(24)
72
+ return nil unless header&.bytesize == 24
73
+ return nil unless header.start_with?(PNG_SIGNATURE)
74
+ return nil unless header.byteslice(12, 4) == "IHDR"
75
+
76
+ [
77
+ header.byteslice(16, 4).unpack1("N"),
78
+ header.byteslice(20, 4).unpack1("N")
79
+ ]
80
+ end
81
+ end
82
+
83
+ def self.jpeg_dimensions(path)
84
+ File.open(path, "rb") do |file|
85
+ return nil unless file.read(2)&.bytes == [0xFF, 0xD8]
86
+
87
+ loop do
88
+ marker = next_jpeg_marker(file)
89
+ return nil unless marker
90
+ return nil if marker == 0xD9 || marker == 0xDA
91
+ next if standalone_jpeg_marker?(marker)
92
+
93
+ length = read_uint16(file)
94
+ return nil unless length && length >= 2
95
+
96
+ if JPEG_START_OF_FRAME_MARKERS.include?(marker)
97
+ frame = file.read(5)
98
+ return nil unless frame&.bytesize == 5
99
+
100
+ return [
101
+ frame.byteslice(3, 2).unpack1("n"),
102
+ frame.byteslice(1, 2).unpack1("n")
103
+ ]
104
+ end
105
+
106
+ file.seek(length - 2, IO::SEEK_CUR)
107
+ end
108
+ end
109
+ end
110
+
111
+ def self.next_jpeg_marker(file)
112
+ loop do
113
+ byte = file.read(1)
114
+ return nil unless byte
115
+ next unless byte.ord == 0xFF
116
+
117
+ loop do
118
+ marker = file.read(1)
119
+ return nil unless marker
120
+ next if marker.ord == 0xFF
121
+
122
+ return marker.ord unless marker.ord == 0x00
123
+ end
124
+ end
125
+ end
126
+
127
+ def self.standalone_jpeg_marker?(marker)
128
+ marker == 0x01 || (0xD0..0xD8).cover?(marker)
129
+ end
130
+
131
+ def self.read_uint16(file)
132
+ bytes = file.read(2)
133
+ return nil unless bytes&.bytesize == 2
134
+
135
+ bytes.unpack1("n")
136
+ end
137
+
138
+ def self.uri_unescape(value)
139
+ URI::RFC2396_PARSER.unescape(value.to_s)
140
+ end
141
+
142
+ private_class_method :normalize_uri,
143
+ :png_dimensions,
144
+ :jpeg_dimensions,
145
+ :next_jpeg_marker,
146
+ :standalone_jpeg_marker?,
147
+ :read_uint16,
148
+ :uri_unescape
149
+ end
150
+ end
151
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SilkLayout
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/silk_layout.rb CHANGED
@@ -10,6 +10,10 @@ module SilkLayout
10
10
  autoload :Node, "silk_layout/html/node"
11
11
  end
12
12
 
13
+ module Resource
14
+ autoload :Image, "silk_layout/resource/image"
15
+ end
16
+
13
17
  module CSS
14
18
  autoload :Parser, "silk_layout/css/parser"
15
19
  autoload :Cascade, "silk_layout/css/cascade"
@@ -18,6 +22,9 @@ module SilkLayout
18
22
  autoload :Declaration, "silk_layout/css/rule"
19
23
  autoload :Selector, "silk_layout/css/selector"
20
24
  autoload :Properties, "silk_layout/css/properties"
25
+ autoload :Values, "silk_layout/css/values"
26
+ autoload :Color, "silk_layout/css/color"
27
+ autoload :PageRule, "silk_layout/css/page_rule"
21
28
  end
22
29
 
23
30
  module Layout
@@ -45,11 +52,54 @@ module SilkLayout
45
52
  autoload :PdfRenderer, "silk_layout/render/pdf_renderer"
46
53
  end
47
54
 
48
- def self.render_document(html_document, out, url: nil)
55
+ def self.render_document(
56
+ html_document,
57
+ out,
58
+ url: nil,
59
+ viewport_width: nil,
60
+ page_size: nil,
61
+ page_width: nil,
62
+ page_height: nil
63
+ )
49
64
  dom, stylesheets = SilkLayout::HTML::Parser.parse_document(html_document, url: url)
50
- rules = SilkLayout::CSS::Parser.parse_all(stylesheets)
51
- box_tree = SilkLayout::Layout::Engine.layout(dom, rules)
65
+ stylesheet = SilkLayout::CSS::Parser.parse_stylesheets(stylesheets, media: :print)
66
+ css_page_size = SilkLayout::CSS::PageRule.resolve_page_size(stylesheet.page_rules)
67
+ resolved_page_size = resolve_page_size(page_size || css_page_size, page_width, page_height)
68
+
69
+ box_tree = SilkLayout::Layout::Engine.layout(
70
+ dom,
71
+ stylesheet.rules,
72
+ viewport_width: viewport_width,
73
+ page_size: resolved_page_size
74
+ )
75
+
76
+ SilkLayout::Render::PdfRenderer.render(
77
+ box_tree,
78
+ out,
79
+ page_size: resolved_page_size
80
+ )
81
+ end
82
+
83
+ def self.resolve_page_size(page_size, page_width, page_height)
84
+ return page_size unless page_width || page_height
85
+
86
+ width, height = page_size_dimensions(page_size)
87
+ {
88
+ width: page_width || width,
89
+ height: page_height || height
90
+ }
91
+ end
92
+ private_class_method :resolve_page_size
52
93
 
53
- SilkLayout::Render::PdfRenderer.render(box_tree, out)
94
+ def self.page_size_dimensions(page_size)
95
+ case page_size
96
+ when Array
97
+ page_size
98
+ when Hash
99
+ [page_size[:width] || page_size["width"], page_size[:height] || page_size["height"]]
100
+ else
101
+ [nil, nil]
102
+ end
54
103
  end
104
+ private_class_method :page_size_dimensions
55
105
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: silk_layout
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Reegan Viljoen
@@ -202,11 +202,14 @@ files:
202
202
  - README.md
203
203
  - lib/silk_layout.rb
204
204
  - lib/silk_layout/css/cascade.rb
205
+ - lib/silk_layout/css/color.rb
205
206
  - lib/silk_layout/css/computed_style.rb
207
+ - lib/silk_layout/css/page_rule.rb
206
208
  - lib/silk_layout/css/parser.rb
207
209
  - lib/silk_layout/css/properties.rb
208
210
  - lib/silk_layout/css/rule.rb
209
211
  - lib/silk_layout/css/selector.rb
212
+ - lib/silk_layout/css/values.rb
210
213
  - lib/silk_layout/html/node.rb
211
214
  - lib/silk_layout/html/parser.rb
212
215
  - lib/silk_layout/layout/block_layout.rb
@@ -222,6 +225,7 @@ files:
222
225
  - lib/silk_layout/render/font_library.rb
223
226
  - lib/silk_layout/render/painter.rb
224
227
  - lib/silk_layout/render/pdf_renderer.rb
228
+ - lib/silk_layout/resource/image.rb
225
229
  - lib/silk_layout/version.rb
226
230
  homepage: https://github.com/reeganviljoen/silk_layout
227
231
  licenses: