cbz_tools 0.2.0 → 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: be6a34e670ac04d6ea7d87875e6ce57eb182dda4f6349f72404ae66ce6a863df
4
- data.tar.gz: 46d9ff6e0b8b52d13027a1ea06558947e6407cc0408637acb9acf9cbe03aafe5
3
+ metadata.gz: 34014f96dd63882bd66f0de3f3a3548faa507e279d37f335fc12ecf8cef3ab41
4
+ data.tar.gz: f54439b98ddad12995b30e74ca1d6e45f559acc5c7df0190af5610623e1911e1
5
5
  SHA512:
6
- metadata.gz: 54063af8baff3b28a84f03d20339d85821f1d0326011faf81afecb4c5081840292a8083a936070d597040cc79ac4833d68c4717fb7a0d0575d68f2fceb823103
7
- data.tar.gz: e264ef924c0781ef429da2aa89ee71dbc9ff7c3d37307bf832dd10e633852e1326af140ced09b2aaf8c4d5185deefd76e6dcf45b0b1f6e09a8989619c8dbc59f
6
+ metadata.gz: 37bc01a7b250c5382507314c57d6c3c208f122c8fd39802ce5308aec16baa07860530e120f37cb5ce69bbf5dcdcc0a145e7a3ce0ad81fc2276ba4ae0e461c835
7
+ data.tar.gz: 9b83a50b99751749896e4d395af34665d85004a49768b7d7b9dac9d578bf4d90fd58190e87c18415620ae3935def84893a61ed40c7dc93ccc682ca31cd819fd5
@@ -1,3 +1,3 @@
1
1
  {
2
- "cSpell.words": ["comicinfo"]
2
+ "cSpell.words": ["comicinfo", "gtin", "penciller"]
3
3
  }
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.3.0] - 2026-08-03
4
+
5
+ - Add `CbzTools::ComicInfo` (parse/build ComicInfo.xml, `SCHEMA`); raises `ParseError` on bad XML.
6
+ - **Breaking**: `Reader#comic_info` returns parsed hash (`{}` when absent), not raw XML.
7
+ - Add `Reader#update_comic_info(metadata)` to write `ComicInfo.xml` back.
8
+ - `Reader#read` is now private; use `first_image`, `image`, `comic_info`.
9
+
3
10
  ## [0.2.0] - 2026-08-03
4
11
 
5
12
  - Add `CbzTools::Reader` for reading CBZ archives (images and `ComicInfo.xml`)
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nokogiri"
4
+
5
+ module CbzTools
6
+ # Reads and writes the ComicInfo.xml metadata embedded in CBZ archives.
7
+ #
8
+ # The two operations are symmetric over SCHEMA: every key on the left
9
+ # side of that hash is a field that {.parse} can read and {.build} can
10
+ # write. Extend the schema in place (for example, in an initializer)
11
+ # if you need to support custom fields.
12
+ module ComicInfo
13
+ class ParseError < Error
14
+ end
15
+
16
+ SCHEMA = {
17
+ title: "Title",
18
+ series: "Series",
19
+ series_sort: "SeriesSort",
20
+ localized_series: "LocalizedSeries",
21
+ number: "Number",
22
+ count: "Count",
23
+ volume: "Volume",
24
+ summary: "Summary",
25
+ notes: "Notes",
26
+ publisher: "Publisher",
27
+ imprint: "Imprint",
28
+ year: "Year",
29
+ month: "Month",
30
+ day: "Day",
31
+ writer: "Writer",
32
+ penciller: "Penciller",
33
+ inker: "Inker",
34
+ colorist: "Colorist",
35
+ letterer: "Letterer",
36
+ cover_artist: "CoverArtist",
37
+ editor: "Editor",
38
+ translator: "Translator",
39
+ genre: "Genre",
40
+ tags: "Tags",
41
+ web: "Web",
42
+ page_count: "PageCount",
43
+ language_iso: "LanguageISO",
44
+ format: "Format",
45
+ series_group: "SeriesGroup",
46
+ age_rating: "AgeRating",
47
+ community_rating: "CommunityRating",
48
+ black_and_white: "BlackAndWhite",
49
+ manga: "Manga",
50
+ characters: "Characters",
51
+ teams: "Teams",
52
+ locations: "Locations",
53
+ scan_information: "ScanInformation",
54
+ story_arc: "StoryArc",
55
+ gtin: "GTIN"
56
+ }
57
+
58
+ # Parses a ComicInfo.xml string into a symbol-keyed hash.
59
+ #
60
+ # Returns an empty hash when the input is empty, or when the XML is
61
+ # valid but has no <ComicInfo> root. Keys not in SCHEMA are ignored.
62
+ # Fields whose text is empty or whitespace-only are dropped.
63
+ #
64
+ # @param xml [String] the ComicInfo.xml document
65
+ # @return [Hash] parsed metadata, keyed by SCHEMA symbols
66
+ # @raise [ParseError] when the XML is malformed
67
+ def self.parse(xml)
68
+ doc = Nokogiri::XML(xml)
69
+ raise ParseError, "Malformed XML: #{doc.errors.first&.message}" if doc.errors.any?
70
+
71
+ root = doc.at("ComicInfo")
72
+ return {} unless root
73
+
74
+ SCHEMA.each_with_object({}) do |(key, tag), hash|
75
+ text = root.at(tag)&.text&.strip
76
+ hash[key] = text if text && !text.empty?
77
+ end
78
+ end
79
+
80
+ # Builds a ComicInfo.xml string from a metadata hash.
81
+ #
82
+ # Accepts symbol or string keys; non-SCHEMA keys are ignored. Values
83
+ # that are +nil+, empty, or whitespace-only are skipped so the
84
+ # resulting XML does not contain empty tags.
85
+ #
86
+ # @param metadata [Hash] field values keyed by SCHEMA symbols or strings
87
+ # @return [String] a ComicInfo.xml document
88
+ def self.build(metadata)
89
+ normalized = metadata.transform_keys(&:to_sym)
90
+ builder = Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml|
91
+ xml.ComicInfo(
92
+ "xmlns:xsd" => "http://www.w3.org/2001/XMLSchema",
93
+ "xmlns:xsi" => "http://www.w3.org/2001/XMLSchema-instance"
94
+ ) do
95
+ SCHEMA.each do |key, tag|
96
+ value = normalized[key]
97
+ next if value.nil? || value.to_s.strip.empty?
98
+
99
+ xml.send(tag, value.to_s)
100
+ end
101
+ end
102
+ end
103
+ builder.to_xml
104
+ end
105
+ end
106
+ end
@@ -2,12 +2,33 @@
2
2
 
3
3
  require "zip"
4
4
 
5
+ require_relative "comic_info"
6
+
5
7
  module CbzTools
8
+ # Reads the contents of a CBZ (Comic Book ZIP) archive.
9
+ #
10
+ # The public surface is image access and parsed ComicInfo metadata;
11
+ # XML is an internal wire format and never appears on the API.
12
+ #
13
+ # @example Reading images and metadata
14
+ # CbzTools::Reader.open("path/to/comic.cbz") do |reader|
15
+ # reader.image_names.first # => "page_001.jpg"
16
+ # reader.first_image # => "<image bytes>"
17
+ # reader.comic_info # => {title: "The Amazing Spider-Man", page_count: "32"}
18
+ # end
6
19
  class Reader
20
+ # File extensions recognized as comic page images.
7
21
  IMAGE_EXTENSIONS = %w[.jpg .jpeg .png .webp].freeze
8
22
 
23
+ # Raised when {#image} is called with a name that does not match
24
+ # any entry in the archive.
9
25
  class ImageNotFoundError < Error; end
10
26
 
27
+ # Opens a CBZ archive, yields a reader, and closes it when the
28
+ # block exits — including on exception.
29
+ #
30
+ # @yieldparam reader [Reader] the open reader
31
+ # @return [void]
11
32
  def self.open(path, &block)
12
33
  reader = new(path)
13
34
  block.call(reader)
@@ -15,16 +36,26 @@ module CbzTools
15
36
  reader&.close
16
37
  end
17
38
 
39
+ # Opens the archive at the given path. Prefer {.open} unless you
40
+ # need to hold the reader open across multiple operations.
41
+ #
42
+ # @param path [String] path to a CBZ file
18
43
  def initialize(path)
19
44
  @path = path
20
45
  @zip = Zip::File.open(@path)
21
46
  end
22
47
 
48
+ # Releases the underlying zip handle. Safe to call more than once.
49
+ #
50
+ # @return [void]
23
51
  def close
24
52
  @zip&.close
25
53
  @zip = nil
26
54
  end
27
55
 
56
+ # Returns the names of all image entries in the archive, sorted.
57
+ #
58
+ # @return [Array<String>]
28
59
  def image_names
29
60
  @zip.entries
30
61
  .reject(&:directory?)
@@ -33,17 +64,19 @@ module CbzTools
33
64
  .sort
34
65
  end
35
66
 
36
- def read(name)
37
- entry = @zip.find_entry(name)
38
- return nil unless entry
39
-
40
- entry.get_input_stream.read
41
- end
42
-
67
+ # Returns the bytes of the first image in the archive, or +nil+
68
+ # when there are no images.
69
+ #
70
+ # @return [String, nil]
43
71
  def first_image
44
72
  read(image_names.first)
45
73
  end
46
74
 
75
+ # Returns the bytes of a specific image by name.
76
+ #
77
+ # @param name [String] entry name within the archive
78
+ # @return [String]
79
+ # @raise [ImageNotFoundError] when no entry with that name exists
47
80
  def image(name)
48
81
  data = read(name)
49
82
  raise ImageNotFoundError, "Image '#{name}' not found in #{@path}" unless data
@@ -51,12 +84,43 @@ module CbzTools
51
84
  data
52
85
  end
53
86
 
87
+ # Returns the parsed ComicInfo metadata as a symbol-keyed hash.
88
+ #
89
+ # Returns an empty hash when the archive has no ComicInfo.xml.
90
+ # The lookup is case-insensitive (+comicinfo.xml+ is also
91
+ # recognized).
92
+ #
93
+ # @return [Hash] parsed metadata, keyed by SCHEMA symbols
94
+ # @raise [CbzTools::ComicInfo::ParseError] when ComicInfo.xml is malformed
54
95
  def comic_info
55
- read("ComicInfo.xml") || read("comicinfo.xml")
96
+ xml = read("ComicInfo.xml") || read("comicinfo.xml")
97
+ xml ? CbzTools::ComicInfo.parse(xml) : {}
98
+ end
99
+
100
+ # Writes the given metadata hash back into the archive as
101
+ # ComicInfo.xml, replacing any existing entry with that name.
102
+ #
103
+ # Accepts symbol or string keys; non-SCHEMA keys are ignored.
104
+ #
105
+ # @param metadata [Hash] field values keyed by SCHEMA symbols or strings
106
+ # @return [void]
107
+ # @raise [Zip::Error] when the archive cannot be opened for writing
108
+ # @raise [SystemCallError] when an underlying I/O operation fails
109
+ def update_comic_info(metadata)
110
+ @zip.get_output_stream("ComicInfo.xml") do |f|
111
+ f.write(CbzTools::ComicInfo.build(metadata))
112
+ end
56
113
  end
57
114
 
58
115
  private
59
116
 
117
+ def read(name)
118
+ entry = @zip.find_entry(name)
119
+ return nil unless entry
120
+
121
+ entry.get_input_stream.read
122
+ end
123
+
60
124
  def image?(name)
61
125
  IMAGE_EXTENSIONS.any? { |ext| name.downcase.end_with?(ext) }
62
126
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CbzTools
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/cbz_tools.rb CHANGED
@@ -3,3 +3,4 @@
3
3
  require_relative "cbz_tools/version"
4
4
  require_relative "cbz_tools/error"
5
5
  require_relative "cbz_tools/reader"
6
+ require_relative "cbz_tools/comic_info"
data/sig/cbz_tools.rbs CHANGED
@@ -1,4 +1,48 @@
1
1
  module CbzTools
2
2
  VERSION: String
3
- # See the writing guide of rbs: https://github.com/ruby/rbs#guides
3
+
4
+ class Error < StandardError
5
+ end
6
+
7
+ class Reader
8
+ IMAGE_EXTENSIONS: Array[String]
9
+
10
+ class ImageNotFoundError < CbzTools::Error
11
+ end
12
+
13
+ def self.open: (String path) { (Reader reader) -> void } -> void
14
+
15
+ def initialize: (String path) -> void
16
+
17
+ def close: -> void
18
+
19
+ def image_names: -> Array[String]
20
+
21
+ def read: (String name) -> String?
22
+
23
+ def first_image: -> String?
24
+
25
+ def image: (String name) -> String
26
+
27
+ def comic_info: -> Hash[Symbol, String]
28
+
29
+ def update_comic_info: (Hash[Symbol | String, String] metadata) -> void
30
+
31
+ private
32
+
33
+ def read: (String name) -> String?
34
+
35
+ def image?: (String name) -> bool
36
+ end
37
+
38
+ module ComicInfo
39
+ SCHEMA: Hash[Symbol, String]
40
+
41
+ class ParseError < CbzTools::Error
42
+ end
43
+
44
+ def self.parse: (String xml) -> Hash[Symbol, String]
45
+
46
+ def self.build: (Hash[Symbol | String, String] metadata) -> String
47
+ end
4
48
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cbz_tools
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Figueroa
@@ -23,6 +23,20 @@ dependencies:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
25
  version: '3.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: nokogiri
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '1.15'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.15'
26
40
  description: 'Tools for working with CBZ comic book archives: reading and extracting
27
41
  their contents, reading and updating their embedded metadata, and converting CBR
28
42
  archives to CBZ.'
@@ -37,6 +51,7 @@ files:
37
51
  - LICENSE.txt
38
52
  - README.md
39
53
  - lib/cbz_tools.rb
54
+ - lib/cbz_tools/comic_info.rb
40
55
  - lib/cbz_tools/error.rb
41
56
  - lib/cbz_tools/reader.rb
42
57
  - lib/cbz_tools/version.rb