From cab2793e0a10fa206e19fd93f943de0c485ba701 Mon Sep 17 00:00:00 2001 From: Raphael Horber Date: Mon, 22 Oct 2018 00:31:04 +0200 Subject: [PATCH] Added Writer. --- README.md | 1 + src/Reader.php | 22 +-- src/Writer.php | 412 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 424 insertions(+), 11 deletions(-) create mode 100644 src/Writer.php diff --git a/README.md b/README.md index 4f241e4..413b4b8 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file ## TODO Features I will add: +* Add more writing variants and expand their docs. * Support more versions (at least V2.3.0). * Improve parsing of non-text frames. * Return detailed info about frames (not only technical name). diff --git a/src/Reader.php b/src/Reader.php index 36ceeb4..b420ce9 100644 --- a/src/Reader.php +++ b/src/Reader.php @@ -31,7 +31,7 @@ class Reader * @access private * @var resource */ - private $_fileHandle; + private $_fileHandle = null; /** * Major version of the source file (4). @@ -39,7 +39,7 @@ class Reader * @access private * @var integer */ - private $_version; + private $_version = 0; /** * Total size of the tag (excluding header, including padding). @@ -47,7 +47,7 @@ class Reader * @access private * @var integer */ - private $_tagSize; + private $_tagSize = 0; /** * Parsed frames of the tag. @@ -55,7 +55,7 @@ class Reader * @access private * @var array[] */ - private $_frames; + private $_frames = []; /** @@ -79,7 +79,7 @@ public function __construct(string $filename) // declare(encoding="UTF-8"); $this->_parseHeader(); - $this->parseFrames(); + $this->_parseFrames(); fclose($this->_fileHandle); } @@ -89,7 +89,7 @@ public function __construct(string $filename) * * Format: * - * [ + * 'identifier' => [ * 'identifier' => 'Four character identifier of the frame', * 'content' => 'Text frames only, parsed content', * 'encoding' => 'Text frames only, text encoding of content', @@ -172,7 +172,7 @@ private function _parseHeader() * @author Raphael Horber * @version 21.10.2018 */ - private function parseFrames() + private function _parseFrames() { $frames = fread($this->_fileHandle, $this->_tagSize); @@ -225,7 +225,7 @@ private function parseFrames() throw new \UnexpectedValueException("Invalid BOM, got: ".bin2hex($bom)); } - $content = substr($content, 2); + $content = substr($content, 2); // $delimiter = "\x00\x00"; break; @@ -257,9 +257,9 @@ private function parseFrames() $this->_frames[$identifier] = [ 'identifier' => $identifier, - 'content' => $content, - 'encoding' => $encoding, - 'raw' => $rawContent, + 'content' => $content, + 'encoding' => $encoding, + 'raw' => $rawContent, ]; // Frame header + frame size. diff --git a/src/Writer.php b/src/Writer.php new file mode 100644 index 0000000..02ef95d --- /dev/null +++ b/src/Writer.php @@ -0,0 +1,412 @@ +_version = $version; + $this->_minTotalTagSize = $minTotalTagSize; + } + + /** + * Writes a new file with the passed frames as ID3 tag. + * + * Format of $frames: + * + * 'identifier (optional)' => [ + * 'identifier' => 'Four character identifier of the frame, if not set as key mandatory', + * 'content' => 'Text frames only, content to write', + * 'encoding' => 'Optional for text frames, encoding to use for writing', + * 'raw' => 'Required for non-text frames, RAW content to write (binary)', + * ] + * + * + * @param array[] $frames Frames to write (modified frames from {@link Reader}). + * @param string $targetFilename Name of the file to write (optionally with path). + * @param string $sourceFilename Name of the content source file + * (content after its ID3 tag will be used as content for the target file). + * + * @return void + * @throws \InvalidArgumentException If the target file handle could not be opened. + * @throws \UnexpectedValueException If a frame requests an unsupported encoding. + * @access public + * @author Raphael Horber + * @version 21.10.2018 + */ + public function writeNewFile(array $frames, string $targetFilename, string $sourceFilename) + { + $this->_targetHandle = @fopen($targetFilename, "wb"); + if ($this->_targetHandle === false) { + throw new \InvalidArgumentException("Target file could not be opened!"); + } + + $this->_parseFrames($frames); + $this->_calculateTagSize(); + + $this->_writeHeader(); + $this->_writeFrames(); + $this->_writePadding(); + + $this->_writeFileContent($sourceFilename); + + fclose($this->_targetHandle); + } + + /** + * Parses and validates the passed frames. + * + * @param array[] $frames Frames to parse/validate. + * + * @return void + * @throws \UnexpectedValueException If a frame requests an unsupported encoding. + * @access private + * @author Raphael Horber + * @version 21.10.2018 + */ + private function _parseFrames(array $frames) + { + // TODO: Throw exceptions/errors instead of continue! (best would be, to display all at once) + foreach ($frames as $identifier => $frame) { + if (isset($frame['identifier']) === true) { + $identifier = $frame['identifier']; + } + + if (strlen($identifier) !== 4) { + continue; + } + + // Text frames must have content, as default encoding UTF-16LE will be used. + // Non-Text frames must have raw element. + if ($identifier{0} === "T") { + if (isset($frame['content']) === false) { + continue; + } + + if (isset($frame['encoding']) === false || $frame['encoding'] === null) { + $frame['encoding'] = "UTF-16LE"; + $frame['content'] = mb_convert_encoding($frame['content'], "UTF-16LE"); + } elseif (in_array($frame['encoding'], self::SUPPORTED_TEXT_ENCODINGS) === false) { + throw new \UnexpectedValueException("Invalid text encoding, got: ".$frame['encoding']); + } + } else { + if (isset($frame['raw']) === false) { + continue; + } + } + + $this->_frames[$identifier] = $frame; + } + } + + /** + * Calculates the resulting tag size. + * + * @return void + * @throws \UnexpectedValueException If a frame requests an unsupported encoding. + * @access private + * @author Raphael Horber + * @version 21.10.2018 + */ + private function _calculateTagSize() + { + foreach ($this->_frames as $identifier => $frame) { + if ($identifier{0} === "T") { + $encoding = $frame['encoding']; + $content = $frame['content']; + + switch ($encoding) { + case "UTF-16LE": + case "UTF-16BE": + // Encoding, BOM, Delimiter; 2 Bytes per character. + $header = 3; + $factor = 2; +// $delimiter = "\x00\x00"; + break; + + case "ISO-8859-1": + case "UTF-8": + // Encoding, no BOM, Delimiter; 1 Byte per character. + $header = 1; + $factor = 1; +// $delimiter = "\x00"; + break; + + + default: + throw new \UnexpectedValueException("Invalid text encoding, got: ".$encoding); + } + + $frameSize = $header + $factor * mb_strlen($content, $encoding); + } else { + $frameSize = strlen($frame['raw']); + } + + $this->_frames[$identifier]['size'] = $frameSize; + + // Frame-Header. + $this->_tagSize += 10; + // Frame-Content. + $this->_tagSize += $frameSize; + } + + if ($this->_minTotalTagSize !== null) { + $paddingSize = $this->_minTotalTagSize - $this->_tagSize - 10; + + if ($paddingSize > 0) { + $this->_paddingSize = $paddingSize; + $this->_tagSize = ($this->_minTotalTagSize - 10); + } + } + } + + /** + * Writes the ID3 header to the target file. + * + * @return void + * @access private + * @author Raphael Horber + * @version 21.10.2018 + */ + private function _writeHeader() + { + // Identifier. + fwrite($this->_targetHandle, "ID3"); + + // TODO: Support/Implement Version 2.3.0. + // Version. + if ($this->_version === 4) { + fwrite($this->_targetHandle, "\x04\x00"); +// } elseif ($this->version === 3) { +// fwrite($this->targetHandle, "\x03\x00"); + } + + // Flags. + fwrite($this->_targetHandle, "\x00"); + + // Tag-Size. + $sizeSynchSafe = Helpers::addSynchSafeBits($this->_tagSize); + fwrite($this->_targetHandle, $sizeSynchSafe); + } + + /** + * Writes the parsed ID3 frames to the target file. + * + * @return void + * @throws \UnexpectedValueException If a frame requests an unsupported encoding. + * @access private + * @author Raphael Horber + * @version 21.10.2018 + */ + private function _writeFrames() + { + foreach ($this->_frames as $identifier => $frame) { + // Identifier. + fwrite($this->_targetHandle, $identifier); + + // Size. + // TODO: Support/Implement Version 2.3.0. + if ($this->_version === 4) { + $sizeSynchSafe = Helpers::addSynchSafeBits($frame['size']); + fwrite($this->_targetHandle, $sizeSynchSafe); +// } elseif ($this->version === 3) { +// $hexSize = base_convert($frame['size'], 10, 16); +// $padded = sprintf("%08s", $hexSize); +// fwrite($this->targetHandle, hex2bin($padded)); + } + + // Flags. + fwrite($this->_targetHandle, "\x00\x00"); + + // Content. + if ($identifier{0} === "T") { + $encoding = $frame['encoding']; + + switch ($encoding) { + case "ISO-8859-1": + $code = "\x00"; + $bom = ""; +// $delimiter = "\x00"; + break; + + case "UTF-16LE": + $code = "\x01"; + $bom = "\xff\xfe"; +// $delimiter = "\x00\x00"; + break; + + case "UTF-16BE": + $code = "\x01"; + $bom = "\xfe\xff"; +// $delimiter = "\x00\x00"; + break; + + case "UTF-8": + $code = "\x03"; + $bom = ""; +// $delimiter = "\x00"; + break; + + default: + throw new \UnexpectedValueException("Invalid text encoding, got: ".$encoding); + } + + // TODO: Support multiple strings per frame (v2.4.0). + $content = $frame['content']; +// fwrite($this->targetHandle, $code.$bom.$content.$delimiter); + fwrite($this->_targetHandle, $code.$bom.$content); + } else { + fwrite($this->_targetHandle, $frame['raw']); + } + } + } + + /** + * Writes the calculated padding, if configured. + * + * @return void + * @access private + * @author Raphael Horber + * @version 21.10.2018 + */ + private function _writePadding() + { + if ($this->_paddingSize <= 0) { + return; + } + + $padding = str_repeat("\x00", $this->_paddingSize); + fwrite($this->_targetHandle, $padding); + } + + /** + * Writes the content from the source file to the target file. + * + * @param string $sourceFilename Name of the content source file. + * + * @return void + * @access private + * @author Raphael Horber + * @version 21.10.2018 + */ + private function _writeFileContent(string $sourceFilename) + { + $sourceFileReader = new Reader($sourceFilename); + $sourceTagSize = $sourceFileReader->getTagSize(); + + $sourceHandle = @fopen($sourceFilename, "rb"); + if ($sourceHandle === false) { + throw new \InvalidArgumentException("Source file could not be opened!"); + } + + // Seek header + tag. + $totalSize = 10 + $sourceTagSize; + fseek($sourceHandle, $totalSize); + + while (feof($sourceHandle) === false) { + $contents = fread($sourceHandle, 8192); + fwrite($this->_targetHandle, $contents); + } + + fclose($sourceHandle); + } +} + + +// Útƒ-8 encoded